Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/web-ui-font-family.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Add font family preferences to Appearance settings (desktop dialog + mobile sheet). The UI/reading font offers Default (Inter), System, Serif, and Custom faces; the code font offers Default (JetBrains Mono), System, and Custom. Custom shows a dropdown of locally installed fonts (detected by probing a curated candidate list, e.g. Maple Mono NF CN) with a manual entry fallback; the default stack is appended as fallback so nothing extra is downloaded. Choices remap the `--font-ui` / `--font-mono` tokens via `<html data-ui-font-family>` / `<html data-code-font-family>`, so every surface picks them up with no component changes; the xterm terminal keeps its fixed literal stack.
16 changes: 16 additions & 0 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,10 @@ function openPr(url: string): void {
:color-scheme="client.colorScheme.value"
:accent="client.accent.value"
:ui-font-size="client.uiFontSize.value"
:ui-font-family="client.uiFontFamily.value"
:ui-custom-font="client.uiCustomFont.value"
:code-font-family="client.codeFontFamily.value"
:code-custom-font="client.codeCustomFont.value"
:auth-ready="client.authReady.value"
:account-model="client.defaultModel.value"
:notify="client.notifyOnComplete.value"
Expand All @@ -1006,6 +1010,10 @@ function openPr(url: string): void {
@set-color-scheme="client.setColorScheme($event)"
@set-accent="client.setAccent($event)"
@set-ui-font-size="client.setUiFontSize($event)"
@set-ui-font-family="client.setUiFontFamily($event)"
@set-ui-custom-font="client.setUiCustomFont($event)"
@set-code-font-family="client.setCodeFontFamily($event)"
@set-code-custom-font="client.setCodeCustomFont($event)"
@set-notify="client.setNotifyOnComplete($event)"
@set-notify-question="client.setNotifyOnQuestion($event)"
@set-notify-approval="client.setNotifyOnApproval($event)"
Expand Down Expand Up @@ -1108,6 +1116,10 @@ function openPr(url: string): void {
:swarm-mode="client.swarmMode.value"
:color-scheme="client.colorScheme.value"
:ui-font-size="client.uiFontSize.value"
:ui-font-family="client.uiFontFamily.value"
:ui-custom-font="client.uiCustomFont.value"
:code-font-family="client.codeFontFamily.value"
:code-custom-font="client.codeCustomFont.value"
:auth-ready="client.authReady.value"
:conversation-toc="client.conversationToc.value"
:server-version="client.serverVersion.value"
Expand All @@ -1118,6 +1130,10 @@ function openPr(url: string): void {
@set-permission="client.setPermission($event)"
@set-color-scheme="client.setColorScheme($event)"
@set-ui-font-size="client.setUiFontSize($event)"
@set-ui-font-family="client.setUiFontFamily($event)"
@set-ui-custom-font="client.setUiCustomFont($event)"
@set-code-font-family="client.setCodeFontFamily($event)"
@set-code-custom-font="client.setCodeCustomFont($event)"
@set-conversation-toc="client.setConversationToc($event)"
@login="() => { showMobileSettings = false; openLogin(); }"
@logout="client.logout"
Expand Down
168 changes: 167 additions & 1 deletion apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type { ConversationStatus, PermissionMode } from '../../types';
import type { AppModel, AppSession, ThinkingLevel } from '../../api/types';
import type { ColorScheme } from '../../composables/useKimiWebClient';
import type { CodeFontFamily, ColorScheme, UiFontFamily } from '../../composables/useKimiWebClient';
import { useKimiWebClient } from '../../composables/useKimiWebClient';
import {
commitLevel,
Expand All @@ -22,9 +22,11 @@ import {
import BottomSheet from '../dialogs/BottomSheet.vue';
import LanguageSwitcher from '../settings/LanguageSwitcher.vue';
import { formatTokens } from '../../lib/formatTokens';
import { CODE_FONT_CANDIDATES, UI_FONT_CANDIDATES, installedCandidates } from '../../lib/fontCandidates';
import Button from '../ui/Button.vue';
import Input from '../ui/Input.vue';
import SegmentedControl from '../ui/SegmentedControl.vue';
import Select from '../ui/Select.vue';

const { t } = useI18n();

Expand All @@ -37,6 +39,10 @@ const props = withDefaults(
swarmMode?: boolean;
colorScheme?: ColorScheme;
uiFontSize?: number;
uiFontFamily?: UiFontFamily;
uiCustomFont?: string;
codeFontFamily?: CodeFontFamily;
codeCustomFont?: string;
authReady?: boolean;
conversationToc?: boolean;
/** Server version from GET /api/v1/meta, shown as a read-only row. */
Expand All @@ -47,6 +53,10 @@ const props = withDefaults(
{
colorScheme: 'system',
uiFontSize: 14,
uiFontFamily: 'default',
uiCustomFont: '',
codeFontFamily: 'default',
codeCustomFont: '',
authReady: false,
serverVersion: '',
models: () => [],
Expand All @@ -62,6 +72,10 @@ const emit = defineEmits<{
setPermission: [mode: PermissionMode];
setColorScheme: [colorScheme: ColorScheme];
setUiFontSize: [size: number];
setUiFontFamily: [font: UiFontFamily];
setUiCustomFont: [name: string];
setCodeFontFamily: [font: CodeFontFamily];
setCodeCustomFont: [name: string];
setConversationToc: [on: boolean];
login: [];
logout: [];
Expand All @@ -71,6 +85,57 @@ function onColorScheme(v: string): void {
emit('setColorScheme', v as ColorScheme);
}

function onUiFontFamily(v: string): void {
emit('setUiFontFamily', v as UiFontFamily);
}

function onCodeFontFamily(v: string): void {
emit('setCodeFontFamily', v as CodeFontFamily);
}

// Custom font dropdowns: the browser cannot enumerate installed fonts, so the
// select probes a curated candidate list (lib/fontCandidates) and offers the
// installed ones; the last option reveals a free-text input for unlisted
// faces. A stored name that is not detected is kept as a select option so the
// current value always renders.
const MANUAL_FONT_VALUE = '__manual__';

const uiFontManual = ref(false);
const codeFontManual = ref(false);

const uiFontOptions = computed<string[]>(() => {
const installed = installedCandidates(UI_FONT_CANDIDATES);
const current = props.uiCustomFont.trim();
return current !== '' && !installed.includes(current) ? [current, ...installed] : installed;
});

const codeFontOptions = computed<string[]>(() => {
const installed = installedCandidates(CODE_FONT_CANDIDATES);
const current = props.codeCustomFont.trim();
return current !== '' && !installed.includes(current) ? [current, ...installed] : installed;
});

const uiFontSelectValue = computed(() => (uiFontManual.value ? MANUAL_FONT_VALUE : props.uiCustomFont));
const codeFontSelectValue = computed(() => (codeFontManual.value ? MANUAL_FONT_VALUE : props.codeCustomFont));

function onUiFontSelect(value: string): void {
if (value === MANUAL_FONT_VALUE) {
uiFontManual.value = true;
return;
}
uiFontManual.value = false;
emit('setUiCustomFont', value);
}

function onCodeFontSelect(value: string): void {
if (value === MANUAL_FONT_VALUE) {
codeFontManual.value = true;
return;
}
codeFontManual.value = false;
emit('setCodeCustomFont', value);
}

const PERM_MODES: PermissionMode[] = ['manual', 'auto', 'yolo'];

// Identity is the model id — display/model names can collide across providers.
Expand Down Expand Up @@ -364,6 +429,103 @@ watch(
</label>
</div>

<div class="srow read-only pref">
<span class="srow-main">
<span class="srow-label">{{ t('theme.fontLabel') }}</span>
</span>
<SegmentedControl
:model-value="uiFontFamily"
:options="[
{ value: 'default', label: t('theme.fontDefault') },
{ value: 'system', label: t('theme.fontSystem') },
{ value: 'serif', label: t('theme.fontSerif') },
{ value: 'custom', label: t('theme.fontCustom') },
]"
@update:model-value="onUiFontFamily"
/>
</div>

<div v-if="uiFontFamily === 'custom'" class="srow read-only pref">
<span class="srow-main">
<span class="srow-label">{{ t('theme.fontCustomLabel') }}</span>
</span>
<span class="font-input">
<Select
size="sm"
:model-value="uiFontSelectValue"
:aria-label="t('theme.fontCustomLabel')"
@update:model-value="onUiFontSelect"
>
<option value="" disabled>{{ t('theme.fontCustomSelect') }}</option>
<option v-for="name in uiFontOptions" :key="name" :value="name">{{ name }}</option>
<option :value="MANUAL_FONT_VALUE">{{ t('theme.fontCustomManual') }}</option>
</Select>
</span>
</div>

<div v-if="uiFontFamily === 'custom' && uiFontManual" class="srow read-only pref">
<span class="srow-main">
<span class="srow-label">{{ t('theme.fontCustomManualLabel') }}</span>
</span>
<span class="font-input">
<Input
size="sm"
:model-value="uiCustomFont"
:placeholder="t('theme.fontCustomPlaceholder')"
:aria-label="t('theme.fontCustomManualLabel')"
@update:model-value="emit('setUiCustomFont', $event)"
/>
</span>
</div>

<div class="srow read-only pref">
<span class="srow-main">
<span class="srow-label">{{ t('theme.codeFontLabel') }}</span>
</span>
<SegmentedControl
:model-value="codeFontFamily"
:options="[
{ value: 'default', label: t('theme.fontDefault') },
{ value: 'system', label: t('theme.fontSystem') },
{ value: 'custom', label: t('theme.fontCustom') },
]"
@update:model-value="onCodeFontFamily"
/>
</div>

<div v-if="codeFontFamily === 'custom'" class="srow read-only pref">
<span class="srow-main">
<span class="srow-label">{{ t('theme.codeFontCustomLabel') }}</span>
</span>
<span class="font-input">
<Select
size="sm"
:model-value="codeFontSelectValue"
:aria-label="t('theme.codeFontCustomLabel')"
@update:model-value="onCodeFontSelect"
>
<option value="" disabled>{{ t('theme.fontCustomSelect') }}</option>
<option v-for="name in codeFontOptions" :key="name" :value="name">{{ name }}</option>
<option :value="MANUAL_FONT_VALUE">{{ t('theme.fontCustomManual') }}</option>
</Select>
</span>
</div>

<div v-if="codeFontFamily === 'custom' && codeFontManual" class="srow read-only pref">
<span class="srow-main">
<span class="srow-label">{{ t('theme.fontCustomManualLabel') }}</span>
</span>
<span class="font-input">
<Input
size="sm"
:model-value="codeCustomFont"
:placeholder="t('theme.fontCustomPlaceholder')"
:aria-label="t('theme.fontCustomManualLabel')"
@update:model-value="emit('setCodeCustomFont', $event)"
/>
</span>
</div>

<button type="button" class="srow" @click="emit('setConversationToc', !conversationToc)">
<span class="srow-main">
<span class="srow-label">{{ t('settings.conversationToc') }}</span>
Expand Down Expand Up @@ -562,6 +724,10 @@ watch(
font-size: var(--ui-font-size-xs);
}

/* Custom font-name input (font family preference): fixed-width field on the
control side of the preference row. */
.font-input { width: 180px; flex: none; }

/* Account rows */
.srow.acct.in .srow-label { color: var(--color-accent-hover); font-weight: 500; }
.srow.acct.out .srow-label { color: var(--color-danger); }
Expand Down
Loading