diff --git a/packages/happy-app/sources/app/(app)/new/index.tsx b/packages/happy-app/sources/app/(app)/new/index.tsx index bc7167d5d..02585b4d9 100644 --- a/packages/happy-app/sources/app/(app)/new/index.tsx +++ b/packages/happy-app/sources/app/(app)/new/index.tsx @@ -65,7 +65,7 @@ const useProfileMap = (profiles: AIBackendProfile[]) => { // Environment variable transformation helper // Returns ALL profile environment variables - daemon will use them as-is -const transformProfileToEnvironmentVars = (profile: AIBackendProfile, agentType: 'claude' | 'codex' | 'gemini' = 'claude') => { +const transformProfileToEnvironmentVars = (profile: AIBackendProfile, agentType: 'claude' | 'codex' | 'gemini' | 'kimi' = 'claude') => { // getProfileEnvironmentVariables already returns ALL env vars from profile // including custom environmentVariables array and provider-specific configs return getProfileEnvironmentVariables(profile); @@ -316,7 +316,7 @@ function NewSessionWizard() { } return 'anthropic'; // Default to Anthropic }); - const [agentType, setAgentType] = React.useState<'claude' | 'codex' | 'gemini'>(() => { + const [agentType, setAgentType] = React.useState<'claude' | 'codex' | 'gemini' | 'kimi'>(() => { // Check if agent type was provided in temp data if (tempSessionData?.agentType) { // Only allow gemini if experiments are enabled @@ -464,7 +464,7 @@ function NewSessionWizard() { if (agentAvailable === false) { // Current agent not available - find first available - const availableAgent: 'claude' | 'codex' | 'gemini' = + const availableAgent: 'claude' | 'codex' | 'gemini' | 'kimi' = cliAvailability.claude === true ? 'claude' : cliAvailability.codex === true ? 'codex' : (cliAvailability.gemini === true && experimentsEnabled) ? 'gemini' : @@ -489,10 +489,10 @@ function NewSessionWizard() { const { variables: daemonEnv } = useEnvironmentVariables(selectedMachineId, envVarRefs); // Temporary banner dismissal (X button) - resets when component unmounts or machine changes - const [hiddenBanners, setHiddenBanners] = React.useState<{ claude: boolean; codex: boolean; gemini: boolean }>({ claude: false, codex: false, gemini: false }); + const [hiddenBanners, setHiddenBanners] = React.useState<{ claude: boolean; codex: boolean; gemini: boolean; kimi: boolean }>({ claude: false, codex: false, gemini: false, kimi: false }); // Helper to check if CLI warning has been dismissed (checks both global and per-machine) - const isWarningDismissed = React.useCallback((cli: 'claude' | 'codex' | 'gemini'): boolean => { + const isWarningDismissed = React.useCallback((cli: 'claude' | 'codex' | 'gemini' | 'kimi'): boolean => { // Check global dismissal first if (dismissedCLIWarnings.global?.[cli] === true) return true; // Check per-machine dismissal @@ -501,7 +501,7 @@ function NewSessionWizard() { }, [selectedMachineId, dismissedCLIWarnings]); // Unified dismiss handler for all three button types (easy to use correctly, hard to use incorrectly) - const handleCLIBannerDismiss = React.useCallback((cli: 'claude' | 'codex' | 'gemini', type: 'temporary' | 'machine' | 'global') => { + const handleCLIBannerDismiss = React.useCallback((cli: 'claude' | 'codex' | 'gemini' | 'kimi', type: 'temporary' | 'machine' | 'global') => { if (type === 'temporary') { // X button: Hide for current session only (not persisted) setHiddenBanners(prev => ({ ...prev, [cli]: true })); @@ -553,7 +553,7 @@ function NewSessionWizard() { const supportedCLIs = (Object.entries(profile.compatibility) as [string, boolean][]) .filter(([, supported]) => supported) .map(([agent]) => agent); - const requiredCLI = supportedCLIs.length === 1 ? supportedCLIs[0] as 'claude' | 'codex' | 'gemini' : null; + const requiredCLI = supportedCLIs.length === 1 ? supportedCLIs[0] as 'claude' | 'codex' | 'gemini' | 'kimi' : null; if (requiredCLI && cliAvailability[requiredCLI] === false) { return { @@ -678,7 +678,7 @@ function NewSessionWizard() { .map(([agent]) => agent); if (supportedCLIs.length === 1) { - const requiredAgent = supportedCLIs[0] as 'claude' | 'codex' | 'gemini'; + const requiredAgent = supportedCLIs[0] as 'claude' | 'codex' | 'gemini' | 'kimi'; // Check if this agent is available and allowed const isAvailable = cliAvailability[requiredAgent] !== false; const isAllowed = requiredAgent !== 'gemini' || experimentsEnabled; @@ -777,7 +777,7 @@ function NewSessionWizard() { name: '', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: Date.now(), updatedAt: Date.now(), diff --git a/packages/happy-app/sources/app/(app)/settings/profiles.tsx b/packages/happy-app/sources/app/(app)/settings/profiles.tsx index fa4522023..b40405335 100644 --- a/packages/happy-app/sources/app/(app)/settings/profiles.tsx +++ b/packages/happy-app/sources/app/(app)/settings/profiles.tsx @@ -43,7 +43,7 @@ function ProfileManager({ onProfileSelect, selectedProfileId }: ProfileManagerPr name: '', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: Date.now(), updatedAt: Date.now(), diff --git a/packages/happy-app/sources/assets/images/icon-kimi.png b/packages/happy-app/sources/assets/images/icon-kimi.png new file mode 100644 index 000000000..3cc0b710a Binary files /dev/null and b/packages/happy-app/sources/assets/images/icon-kimi.png differ diff --git a/packages/happy-app/sources/assets/images/icon-kimi@2x.png b/packages/happy-app/sources/assets/images/icon-kimi@2x.png new file mode 100644 index 000000000..312dcc6d0 Binary files /dev/null and b/packages/happy-app/sources/assets/images/icon-kimi@2x.png differ diff --git a/packages/happy-app/sources/assets/images/icon-kimi@3x.png b/packages/happy-app/sources/assets/images/icon-kimi@3x.png new file mode 100644 index 000000000..53b7f65f7 Binary files /dev/null and b/packages/happy-app/sources/assets/images/icon-kimi@3x.png differ diff --git a/packages/happy-app/sources/components/AgentInput.tsx b/packages/happy-app/sources/components/AgentInput.tsx index bb312aabb..3734dae6f 100644 --- a/packages/happy-app/sources/components/AgentInput.tsx +++ b/packages/happy-app/sources/components/AgentInput.tsx @@ -65,7 +65,7 @@ interface AgentInputProps { }; alwaysShowContextSize?: boolean; onFileViewerPress?: () => void; - agentType?: 'claude' | 'codex' | 'gemini'; + agentType?: 'claude' | 'codex' | 'gemini' | 'kimi'; onAgentClick?: () => void; machineName?: string | null; onMachineClick?: () => void; diff --git a/packages/happy-app/sources/components/Avatar.tsx b/packages/happy-app/sources/components/Avatar.tsx index fe78d57ca..b34dcd00c 100644 --- a/packages/happy-app/sources/components/Avatar.tsx +++ b/packages/happy-app/sources/components/Avatar.tsx @@ -22,6 +22,7 @@ const flavorIcons = { claude: require('@/assets/images/icon-claude.png'), codex: require('@/assets/images/icon-gpt.png'), gemini: require('@/assets/images/icon-gemini.png'), + kimi: require('@/assets/images/icon-kimi.png'), }; const styles = StyleSheet.create((theme) => ({ diff --git a/packages/happy-app/sources/components/NewSessionWizard.tsx b/packages/happy-app/sources/components/NewSessionWizard.tsx index 8b0da8e88..8fcfce332 100644 --- a/packages/happy-app/sources/components/NewSessionWizard.tsx +++ b/packages/happy-app/sources/components/NewSessionWizard.tsx @@ -562,7 +562,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N description: 'Default Claude configuration', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: false, gemini: false }, + compatibility: { claude: true, codex: false, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -581,7 +581,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N { name: 'ANTHROPIC_SMALL_FAST_MODEL', value: 'deepseek-chat' }, { name: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', value: '1' }, ], - compatibility: { claude: true, codex: false, gemini: false }, + compatibility: { claude: true, codex: false, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -596,7 +596,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N model: 'gpt-4-turbo', }, environmentVariables: [], - compatibility: { claude: false, codex: true, gemini: false }, + compatibility: { claude: false, codex: true, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -612,7 +612,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N deploymentName: 'gpt-4-turbo', }, environmentVariables: [], - compatibility: { claude: false, codex: true, gemini: false }, + compatibility: { claude: false, codex: true, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -628,7 +628,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N environmentVariables: [ { name: 'AZURE_OPENAI_API_VERSION', value: '2024-02-15-preview' }, ], - compatibility: { claude: false, codex: true, gemini: false }, + compatibility: { claude: false, codex: true, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -643,7 +643,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N model: 'glm-4.6', }, environmentVariables: [], - compatibility: { claude: true, codex: false, gemini: false }, + compatibility: { claude: true, codex: false, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -658,7 +658,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N model: 'gpt-4-turbo', }, environmentVariables: [], - compatibility: { claude: false, codex: true, gemini: false }, + compatibility: { claude: false, codex: true, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -937,7 +937,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N description: 'Custom AI profile', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: Date.now(), updatedAt: Date.now(), diff --git a/packages/happy-app/sources/components/modelModeOptions.ts b/packages/happy-app/sources/components/modelModeOptions.ts index 6d86d5607..908783467 100644 --- a/packages/happy-app/sources/components/modelModeOptions.ts +++ b/packages/happy-app/sources/components/modelModeOptions.ts @@ -93,6 +93,23 @@ export function getGeminiModelModes(): ModelMode[] { return GEMINI_MODEL_FALLBACKS; } +export function getKimiPermissionModes(translate: Translate): PermissionMode[] { + return [ + { key: 'default', name: translate('agentInput.kimiPermissionMode.default'), description: null }, + { key: 'read-only', name: translate('agentInput.kimiPermissionMode.readOnly'), description: null }, + { key: 'safe-yolo', name: translate('agentInput.kimiPermissionMode.safeYolo'), description: null }, + { key: 'yolo', name: translate('agentInput.kimiPermissionMode.yolo'), description: null }, + ]; +} + +export function getKimiModelModes(): ModelMode[] { + return [ + { key: 'kimi-k2-0711-preview', name: 'Kimi K2', description: 'Most capable' }, + { key: 'kimi-k1.6-preview', name: 'Kimi K1.6', description: 'Fast & capable' }, + { key: 'kimi-k1.5-preview', name: 'Kimi K1.5', description: 'Fast & efficient' }, + ]; +} + export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Translate): PermissionMode[] { if (flavor === 'codex') { return getCodexPermissionModes(translate); @@ -100,6 +117,9 @@ export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Tran if (flavor === 'gemini') { return getGeminiPermissionModes(translate); } + if (flavor === 'kimi') { + return getKimiPermissionModes(translate); + } return getClaudePermissionModes(translate); } @@ -110,6 +130,9 @@ export function getHardcodedModelModes(flavor: AgentFlavor, translate: Translate if (flavor === 'gemini') { return getGeminiModelModes(); } + if (flavor === 'kimi') { + return getKimiModelModes(); + } return getClaudeModelModes(); } @@ -130,7 +153,7 @@ export function getAvailablePermissionModes( metadata: Metadata | null | undefined, translate: Translate, ): PermissionMode[] { - if (flavor === 'claude' || flavor === 'codex') { + if (flavor === 'claude' || flavor === 'codex' || flavor === 'kimi') { return hackModes(getHardcodedPermissionModes(flavor, translate)); } @@ -169,6 +192,9 @@ export function getDefaultModelKey(flavor: AgentFlavor): string { if (flavor === 'gemini') { return 'gemini-2.5-pro'; } + if (flavor === 'kimi') { + return 'kimi-k2-0711-preview'; + } return 'default'; } diff --git a/packages/happy-app/sources/hooks/useCLIDetection.ts b/packages/happy-app/sources/hooks/useCLIDetection.ts index bda5c547b..3be5b5e93 100644 --- a/packages/happy-app/sources/hooks/useCLIDetection.ts +++ b/packages/happy-app/sources/hooks/useCLIDetection.ts @@ -5,6 +5,7 @@ interface CLIAvailability { claude: boolean | null; // null = unknown/loading, true = installed, false = not installed codex: boolean | null; gemini: boolean | null; + kimi: boolean | null; isDetecting: boolean; // Explicit loading state timestamp: number; // When detection completed error?: string; // Detection error message (for debugging) @@ -37,13 +38,14 @@ export function useCLIDetection(machineId: string | null): CLIAvailability { claude: null, codex: null, gemini: null, + kimi: null, isDetecting: false, timestamp: 0, }); useEffect(() => { if (!machineId) { - setAvailability({ claude: null, codex: null, gemini: null, isDetecting: false, timestamp: 0 }); + setAvailability({ claude: null, codex: null, gemini: null, kimi: null, isDetecting: false, timestamp: 0 }); return; } @@ -71,12 +73,12 @@ export function useCLIDetection(machineId: string | null): CLIAvailability { if (result.success && result.exitCode === 0) { // Parse output: "claude:true\ncodex:false\ngemini:false" const lines = result.stdout.trim().split('\n'); - const cliStatus: { claude?: boolean; codex?: boolean; gemini?: boolean } = {}; + const cliStatus: { claude?: boolean; codex?: boolean; gemini?: boolean; kimi?: boolean } = {}; lines.forEach(line => { const [cli, status] = line.split(':'); if (cli && status) { - cliStatus[cli.trim() as 'claude' | 'codex' | 'gemini'] = status.trim() === 'true'; + cliStatus[cli.trim() as 'claude' | 'codex' | 'gemini' | 'kimi'] = status.trim() === 'true'; } }); @@ -85,6 +87,7 @@ export function useCLIDetection(machineId: string | null): CLIAvailability { claude: cliStatus.claude ?? null, codex: cliStatus.codex ?? null, gemini: cliStatus.gemini ?? null, + kimi: cliStatus.kimi ?? null, isDetecting: false, timestamp: Date.now(), }); @@ -95,6 +98,7 @@ export function useCLIDetection(machineId: string | null): CLIAvailability { claude: null, codex: null, gemini: null, + kimi: null, isDetecting: false, timestamp: 0, error: `Detection failed: ${result.stderr || 'Unknown error'}`, @@ -109,6 +113,7 @@ export function useCLIDetection(machineId: string | null): CLIAvailability { claude: null, codex: null, gemini: null, + kimi: null, isDetecting: false, timestamp: 0, error: error instanceof Error ? error.message : 'Detection error', diff --git a/packages/happy-app/sources/hooks/useConnectTerminal.ts b/packages/happy-app/sources/hooks/useConnectTerminal.ts index 43a9d537a..1f0d86837 100644 --- a/packages/happy-app/sources/hooks/useConnectTerminal.ts +++ b/packages/happy-app/sources/hooks/useConnectTerminal.ts @@ -25,27 +25,39 @@ export function useConnectTerminal(options?: UseConnectTerminalOptions) { Modal.alert(t('common.error'), t('modals.invalidAuthUrl'), [{ text: t('common.ok') }]); return false; } - + + if (!auth.credentials?.secret || !auth.credentials?.token) { + console.error('[ConnectTerminal] Missing auth credentials'); + Modal.alert(t('common.error'), t('modals.failedToConnectTerminal'), [{ text: t('common.ok') }]); + return false; + } + + if (!sync.encryption?.contentDataKey) { + console.error('[ConnectTerminal] Encryption not initialized'); + Modal.alert(t('common.error'), t('modals.failedToConnectTerminal'), [{ text: t('common.ok') }]); + return false; + } + setIsLoading(true); try { const tail = url.slice('happy://terminal?'.length); const publicKey = decodeBase64(tail, 'base64url'); - const responseV1 = encryptBox(decodeBase64(auth.credentials!.secret, 'base64url'), publicKey); + const responseV1 = encryptBox(decodeBase64(auth.credentials.secret, 'base64url'), publicKey); let responseV2Bundle = new Uint8Array(sync.encryption.contentDataKey.length + 1); responseV2Bundle[0] = 0; responseV2Bundle.set(sync.encryption.contentDataKey, 1); const responseV2 = encryptBox(responseV2Bundle, publicKey); - await authApprove(auth.credentials!.token, publicKey, responseV1, responseV2); - + await authApprove(auth.credentials.token, publicKey, responseV1, responseV2); + Modal.alert(t('common.success'), t('modals.terminalConnectedSuccessfully'), [ - { - text: t('common.ok'), + { + text: t('common.ok'), onPress: () => options?.onSuccess?.() } ]); return true; } catch (e) { - console.error(e); + console.error('[ConnectTerminal] Error:', e); Modal.alert(t('common.error'), t('modals.failedToConnectTerminal'), [{ text: t('common.ok') }]); options?.onError?.(e); return false; diff --git a/packages/happy-app/sources/sync/ops.ts b/packages/happy-app/sources/sync/ops.ts index 07f70e694..2ea6a6ba0 100644 --- a/packages/happy-app/sources/sync/ops.ts +++ b/packages/happy-app/sources/sync/ops.ts @@ -138,7 +138,7 @@ export interface SpawnSessionOptions { directory: string; approvedNewDirectoryCreation?: boolean; token?: string; - agent?: 'codex' | 'claude' | 'gemini'; + agent?: 'codex' | 'claude' | 'gemini' | 'kimi'; // Environment variables from AI backend profile // Accepts any environment variables - daemon will pass them to the agent process // Common variables include: @@ -167,7 +167,7 @@ export async function machineSpawnNewSession(options: SpawnSessionOptions): Prom directory: string approvedNewDirectoryCreation?: boolean, token?: string, - agent?: 'codex' | 'claude' | 'gemini', + agent?: 'codex' | 'claude' | 'gemini' | 'kimi', environmentVariables?: Record; }>( machineId, diff --git a/packages/happy-app/sources/sync/persistence.ts b/packages/happy-app/sources/sync/persistence.ts index 100c9ee3a..8ecc81f52 100644 --- a/packages/happy-app/sources/sync/persistence.ts +++ b/packages/happy-app/sources/sync/persistence.ts @@ -8,7 +8,7 @@ import type { PermissionModeKey } from '@/components/PermissionModeSelector'; const mmkv = new MMKV(); const NEW_SESSION_DRAFT_KEY = 'new-session-draft-v1'; -export type NewSessionAgentType = 'claude' | 'codex' | 'gemini'; +export type NewSessionAgentType = 'claude' | 'codex' | 'gemini' | 'kimi'; export type NewSessionSessionType = 'simple' | 'worktree'; export interface NewSessionDraft { diff --git a/packages/happy-app/sources/sync/profileUtils.ts b/packages/happy-app/sources/sync/profileUtils.ts index d90a98a93..73625e036 100644 --- a/packages/happy-app/sources/sync/profileUtils.ts +++ b/packages/happy-app/sources/sync/profileUtils.ts @@ -245,7 +245,7 @@ export const getBuiltInProfile = (id: string): AIBackendProfile | null => { anthropicConfig: {}, environmentVariables: [], defaultPermissionMode: 'default', - compatibility: { claude: true, codex: false, gemini: false }, + compatibility: { claude: true, codex: false, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -270,7 +270,7 @@ export const getBuiltInProfile = (id: string): AIBackendProfile | null => { { name: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', value: '${DEEPSEEK_CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:-1}' }, ], defaultPermissionMode: 'default', - compatibility: { claude: true, codex: false, gemini: false }, + compatibility: { claude: true, codex: false, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -297,7 +297,7 @@ export const getBuiltInProfile = (id: string): AIBackendProfile | null => { { name: 'ANTHROPIC_DEFAULT_HAIKU_MODEL', value: '${Z_AI_HAIKU_MODEL:-GLM-4.5-Air}' }, ], defaultPermissionMode: 'default', - compatibility: { claude: true, codex: false, gemini: false }, + compatibility: { claude: true, codex: false, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -316,7 +316,7 @@ export const getBuiltInProfile = (id: string): AIBackendProfile | null => { { name: 'API_TIMEOUT_MS', value: '600000' }, { name: 'CODEX_SMALL_FAST_MODEL', value: 'gpt-5-codex-low' }, ], - compatibility: { claude: false, codex: true, gemini: false }, + compatibility: { claude: false, codex: true, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), @@ -333,7 +333,7 @@ export const getBuiltInProfile = (id: string): AIBackendProfile | null => { { name: 'OPENAI_API_TIMEOUT_MS', value: '600000' }, { name: 'API_TIMEOUT_MS', value: '600000' }, ], - compatibility: { claude: false, codex: true, gemini: false }, + compatibility: { claude: false, codex: true, gemini: false, kimi: false }, isBuiltIn: true, createdAt: Date.now(), updatedAt: Date.now(), diff --git a/packages/happy-app/sources/sync/settings.spec.ts b/packages/happy-app/sources/sync/settings.spec.ts index 4f36ce46f..0d0d63dea 100644 --- a/packages/happy-app/sources/sync/settings.spec.ts +++ b/packages/happy-app/sources/sync/settings.spec.ts @@ -562,7 +562,7 @@ describe('settings', () => { name: 'Server Profile', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: Date.now(), updatedAt: Date.now(), @@ -580,7 +580,7 @@ describe('settings', () => { name: 'Local Profile', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: Date.now(), updatedAt: Date.now(), @@ -682,7 +682,7 @@ describe('settings', () => { name: 'Test', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: Date.now(), updatedAt: Date.now(), @@ -846,7 +846,7 @@ describe('settings', () => { name: 'Local Profile', anthropicConfig: {}, environmentVariables: [], - compatibility: { claude: true, codex: true, gemini: true }, + compatibility: { claude: true, codex: true, gemini: true, kimi: true }, isBuiltIn: false, createdAt: 2000, updatedAt: 2000, diff --git a/packages/happy-app/sources/sync/settings.ts b/packages/happy-app/sources/sync/settings.ts index 5746c863d..70ff8e7a7 100644 --- a/packages/happy-app/sources/sync/settings.ts +++ b/packages/happy-app/sources/sync/settings.ts @@ -88,6 +88,7 @@ const ProfileCompatibilitySchema = z.object({ claude: z.boolean().default(true), codex: z.boolean().default(true), gemini: z.boolean().default(true), + kimi: z.boolean().default(true), }); export const AIBackendProfileSchema = z.object({ @@ -122,7 +123,7 @@ export const AIBackendProfileSchema = z.object({ defaultModelMode: z.string().optional(), // Compatibility metadata - compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true }), + compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true, kimi: true }), // Built-in profile indicator isBuiltIn: z.boolean().default(false), @@ -136,7 +137,7 @@ export const AIBackendProfileSchema = z.object({ export type AIBackendProfile = z.infer; // Helper functions for profile validation and compatibility -export function validateProfileForAgent(profile: AIBackendProfile, agent: 'claude' | 'codex' | 'gemini'): boolean { +export function validateProfileForAgent(profile: AIBackendProfile, agent: 'claude' | 'codex' | 'gemini' | 'kimi'): boolean { return profile.compatibility[agent]; } @@ -294,11 +295,13 @@ export const SettingsSchema = z.object({ claude: z.boolean().optional(), codex: z.boolean().optional(), gemini: z.boolean().optional(), + kimi: z.boolean().optional(), })).default({}), global: z.object({ claude: z.boolean().optional(), codex: z.boolean().optional(), gemini: z.boolean().optional(), + kimi: z.boolean().optional(), }).default({}), }).default({ perMachine: {}, global: {} }).describe('Tracks which CLI installation warnings user has dismissed (per-machine or globally)'), }); diff --git a/packages/happy-app/sources/sync/typesRaw.ts b/packages/happy-app/sources/sync/typesRaw.ts index 5ac33bb0f..0dca2d06f 100644 --- a/packages/happy-app/sources/sync/typesRaw.ts +++ b/packages/happy-app/sources/sync/typesRaw.ts @@ -316,7 +316,7 @@ const rawAgentRecordSchema = z.discriminatedUnion('type', [z.object({ }), z.object({ // ACP (Agent Communication Protocol) - unified format for all agent providers type: z.literal('acp'), - provider: z.enum(['gemini', 'codex', 'claude', 'opencode']), + provider: z.enum(['gemini', 'codex', 'claude', 'opencode', 'kimi']), data: z.discriminatedUnion('type', [ // Core message types z.object({ type: z.literal('reasoning'), message: z.string() }), diff --git a/packages/happy-app/sources/text/_default.ts b/packages/happy-app/sources/text/_default.ts index c3a0d673a..02f3eb488 100644 --- a/packages/happy-app/sources/text/_default.ts +++ b/packages/happy-app/sources/text/_default.ts @@ -403,6 +403,7 @@ export const en = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODEL', @@ -438,6 +439,16 @@ export const en = { badgeSafeYolo: 'Safe YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'KIMI PERMISSION MODE', + default: 'Default', + readOnly: 'Read Only', + safeYolo: 'Safe YOLO', + yolo: 'YOLO', + badgeReadOnly: 'Read Only', + badgeSafeYolo: 'Safe YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `${percent}% left`, }, diff --git a/packages/happy-app/sources/text/translations/ca.ts b/packages/happy-app/sources/text/translations/ca.ts index 76d928500..648b5483a 100644 --- a/packages/happy-app/sources/text/translations/ca.ts +++ b/packages/happy-app/sources/text/translations/ca.ts @@ -404,6 +404,7 @@ export const ca: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODEL', @@ -439,6 +440,16 @@ export const ca: TranslationStructure = { badgeSafeYolo: 'YOLO segur', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'MODE DE PERMISOS KIMI', + default: 'Predeterminat', + readOnly: 'Només Lectura', + safeYolo: 'YOLO Segur', + yolo: 'YOLO', + badgeReadOnly: 'Només Lectura', + badgeSafeYolo: 'YOLO Segur', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `${percent}% restant`, }, diff --git a/packages/happy-app/sources/text/translations/en.ts b/packages/happy-app/sources/text/translations/en.ts index b86aa4af6..78a6ff0d7 100644 --- a/packages/happy-app/sources/text/translations/en.ts +++ b/packages/happy-app/sources/text/translations/en.ts @@ -419,6 +419,7 @@ export const en: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODEL', @@ -454,6 +455,16 @@ export const en: TranslationStructure = { badgeSafeYolo: 'Safe YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'KIMI PERMISSION MODE', + default: 'Default', + readOnly: 'Read Only', + safeYolo: 'Safe YOLO', + yolo: 'YOLO', + badgeReadOnly: 'Read Only', + badgeSafeYolo: 'Safe YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `${percent}% left`, }, diff --git a/packages/happy-app/sources/text/translations/es.ts b/packages/happy-app/sources/text/translations/es.ts index 41c17664e..0f54a2df3 100644 --- a/packages/happy-app/sources/text/translations/es.ts +++ b/packages/happy-app/sources/text/translations/es.ts @@ -404,6 +404,7 @@ export const es: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODELO', @@ -439,6 +440,16 @@ export const es: TranslationStructure = { badgeSafeYolo: 'YOLO seguro', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'MODO DE PERMISOS KIMI', + default: 'Predeterminado', + readOnly: 'Solo Lectura', + safeYolo: 'YOLO Seguro', + yolo: 'YOLO', + badgeReadOnly: 'Solo Lectura', + badgeSafeYolo: 'YOLO Seguro', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `${percent}% restante`, }, diff --git a/packages/happy-app/sources/text/translations/it.ts b/packages/happy-app/sources/text/translations/it.ts index 6dbfb52ac..13a446b0b 100644 --- a/packages/happy-app/sources/text/translations/it.ts +++ b/packages/happy-app/sources/text/translations/it.ts @@ -433,6 +433,7 @@ export const it: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODELLO', @@ -468,6 +469,16 @@ export const it: TranslationStructure = { badgeSafeYolo: 'YOLO sicuro', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'MODALITÀ PERMESSI KIMI', + default: 'Predefinito', + readOnly: 'Sola Lettura', + safeYolo: 'YOLO Sicuro', + yolo: 'YOLO', + badgeReadOnly: 'Sola Lettura', + badgeSafeYolo: 'YOLO Sicuro', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `${percent}% restante`, }, diff --git a/packages/happy-app/sources/text/translations/ja.ts b/packages/happy-app/sources/text/translations/ja.ts index 090480d7a..126e67caa 100644 --- a/packages/happy-app/sources/text/translations/ja.ts +++ b/packages/happy-app/sources/text/translations/ja.ts @@ -436,6 +436,7 @@ export const ja: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'モデル', @@ -471,6 +472,16 @@ export const ja: TranslationStructure = { badgeSafeYolo: '安全YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'KIMI 権限モード', + default: 'デフォルト', + readOnly: '読み取り専用', + safeYolo: 'セーフ YOLO', + yolo: 'YOLO', + badgeReadOnly: '読み取り専用', + badgeSafeYolo: 'セーフ YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `残り ${percent}%`, }, diff --git a/packages/happy-app/sources/text/translations/pl.ts b/packages/happy-app/sources/text/translations/pl.ts index 55401af6a..087b7a35f 100644 --- a/packages/happy-app/sources/text/translations/pl.ts +++ b/packages/happy-app/sources/text/translations/pl.ts @@ -414,6 +414,7 @@ export const pl: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODEL', @@ -449,6 +450,16 @@ export const pl: TranslationStructure = { badgeSafeYolo: 'Bezpieczny YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'TRYB UPRAWNIEŃ KIMI', + default: 'Domyślny', + readOnly: 'Tylko odczyt', + safeYolo: 'Bezpieczny YOLO', + yolo: 'YOLO', + badgeReadOnly: 'Tylko odczyt', + badgeSafeYolo: 'Bezpieczny YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `Pozostało ${percent}%`, }, diff --git a/packages/happy-app/sources/text/translations/pt.ts b/packages/happy-app/sources/text/translations/pt.ts index 75d9afed2..53976e401 100644 --- a/packages/happy-app/sources/text/translations/pt.ts +++ b/packages/happy-app/sources/text/translations/pt.ts @@ -404,6 +404,7 @@ export const pt: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'MODELO', @@ -439,6 +440,16 @@ export const pt: TranslationStructure = { badgeSafeYolo: 'YOLO seguro', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'MODO DE PERMISSÃO KIMI', + default: 'Padrão', + readOnly: 'Somente Leitura', + safeYolo: 'YOLO Seguro', + yolo: 'YOLO', + badgeReadOnly: 'Somente Leitura', + badgeSafeYolo: 'YOLO Seguro', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `${percent}% restante`, }, diff --git a/packages/happy-app/sources/text/translations/ru.ts b/packages/happy-app/sources/text/translations/ru.ts index 930474bb7..9ae371711 100644 --- a/packages/happy-app/sources/text/translations/ru.ts +++ b/packages/happy-app/sources/text/translations/ru.ts @@ -414,6 +414,7 @@ export const ru: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: 'МОДЕЛЬ', @@ -449,6 +450,16 @@ export const ru: TranslationStructure = { badgeSafeYolo: 'Безопасный YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'РЕЖИМ РАЗРЕШЕНИЙ KIMI', + default: 'По умолчанию', + readOnly: 'Только чтение', + safeYolo: 'Безопасный YOLO', + yolo: 'YOLO', + badgeReadOnly: 'Только чтение', + badgeSafeYolo: 'Безопасный YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `Осталось ${percent}%`, }, diff --git a/packages/happy-app/sources/text/translations/zh-Hans.ts b/packages/happy-app/sources/text/translations/zh-Hans.ts index a9db3ead3..ae09d121f 100644 --- a/packages/happy-app/sources/text/translations/zh-Hans.ts +++ b/packages/happy-app/sources/text/translations/zh-Hans.ts @@ -406,6 +406,7 @@ export const zhHans: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: '模型', @@ -441,6 +442,16 @@ export const zhHans: TranslationStructure = { badgeSafeYolo: '安全 YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'KIMI 权限模式', + default: '默认', + readOnly: '只读', + safeYolo: '安全 YOLO', + yolo: 'YOLO', + badgeReadOnly: '只读', + badgeSafeYolo: '安全 YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `剩余 ${percent}%`, }, diff --git a/packages/happy-app/sources/text/translations/zh-Hant.ts b/packages/happy-app/sources/text/translations/zh-Hant.ts index e09ca6f3c..caad5980c 100644 --- a/packages/happy-app/sources/text/translations/zh-Hant.ts +++ b/packages/happy-app/sources/text/translations/zh-Hant.ts @@ -405,6 +405,7 @@ export const zhHant: TranslationStructure = { claude: 'Claude', codex: 'Codex', gemini: 'Gemini', + kimi: 'Kimi', }, model: { title: '模型', @@ -440,6 +441,16 @@ export const zhHant: TranslationStructure = { badgeSafeYolo: '安全 YOLO', badgeYolo: 'YOLO', }, + kimiPermissionMode: { + title: 'KIMI 權限模式', + default: '預設', + readOnly: '唯讀', + safeYolo: '安全 YOLO', + yolo: 'YOLO', + badgeReadOnly: '唯讀', + badgeSafeYolo: '安全 YOLO', + badgeYolo: 'YOLO', + }, context: { remaining: ({ percent }: { percent: number }) => `剩餘 ${percent}%`, }, diff --git a/packages/happy-app/sources/utils/tempDataStore.ts b/packages/happy-app/sources/utils/tempDataStore.ts index d120daf31..dafa253c2 100644 --- a/packages/happy-app/sources/utils/tempDataStore.ts +++ b/packages/happy-app/sources/utils/tempDataStore.ts @@ -9,7 +9,7 @@ export interface NewSessionData { prompt?: string; machineId?: string; path?: string; - agentType?: 'claude' | 'codex' | 'gemini'; + agentType?: 'claude' | 'codex' | 'gemini' | 'kimi'; sessionType?: 'simple' | 'worktree'; taskId?: string; taskTitle?: string;