Skip to content
Draft
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/tidy-planets-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Add an opt-in setting to render $…$ as inline LaTeX in chat messages; it stays off by default because bare $ in prices or variables like $PATH can be misdetected as math. Enable it in Settings → General → Inline math rendering.
4 changes: 4 additions & 0 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ function openPr(url: string): void {
:notify-permission="client.notifyPermission.value"
:sound="client.soundOnComplete.value"
:conversation-toc="client.conversationToc.value"
:inline-math="client.inlineMath.value"
:config="client.config.value"
:models="client.models.value"
:config-saving="configSaving"
Expand All @@ -1011,6 +1012,7 @@ function openPr(url: string): void {
@set-notify-approval="client.setNotifyOnApproval($event)"
@set-sound="client.setSoundOnComplete($event)"
@set-conversation-toc="client.setConversationToc($event)"
@set-inline-math="client.setInlineMath($event)"
@update-config="handleUpdateConfig($event)"
@login="() => { showSettings = false; openLogin(); }"
@logout="client.logout"
Expand Down Expand Up @@ -1110,6 +1112,7 @@ function openPr(url: string): void {
:ui-font-size="client.uiFontSize.value"
:auth-ready="client.authReady.value"
:conversation-toc="client.conversationToc.value"
:inline-math="client.inlineMath.value"
:server-version="client.serverVersion.value"
@pick-model="openModelPicker()"
@set-thinking="client.setThinking($event)"
Expand All @@ -1119,6 +1122,7 @@ function openPr(url: string): void {
@set-color-scheme="client.setColorScheme($event)"
@set-ui-font-size="client.setUiFontSize($event)"
@set-conversation-toc="client.setConversationToc($event)"
@set-inline-math="client.setInlineMath($event)"
@login="() => { showMobileSettings = false; openLogin(); }"
@logout="client.logout"
/>
Expand Down
21 changes: 13 additions & 8 deletions apps/kimi-web/src/components/chat/Markdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
clearMermaidWorker,
} from 'markstream-vue';
import type { MarkdownIt } from 'markstream-vue';
import { useKimiWebClient } from '../../composables/useKimiWebClient';
import { configureMathRules } from '../../lib/markdownMath';
import { useIsDark } from '../../composables/useIsDark';
import type { FilePreviewRequest } from '../../types';
import { collectFilePathAliases, findFilePathLinks } from '../../lib/filePathLinks';
Expand Down Expand Up @@ -64,13 +66,15 @@ clearMermaidWorker();
setKaTeXWorker(new katexWorkerModule.default());
setMermaidWorker(new mermaidWorkerModule.default());

// Only `$$…$$` display math is rendered; single `$` inline math is disabled so
// prices, env vars, and shell paths (`$5`, `$PATH`, `$HOME/bin`) stay literal
// without any escaping or code-detection gymnastics. `math_block` (the $$ rule)
// is left enabled.
function disableInlineMath(md: MarkdownIt): MarkdownIt {
md.inline.ruler.disable('math');
return md;
// Only `$$…$$` display math (the `math_block` rule) is always rendered.
// Single-`$` inline math is OPT-IN (Settings → inline math): the rule can
// misdetect prices, env vars, and shell paths (`$5`, `$PATH`, `$HOME/bin`)
// as math and swallow them into a broken formula, so it stays disabled
// unless the user turns it on. The MarkdownRender instances are keyed on the
// preference below so toggling it re-parses the message.
const client = useKimiWebClient();
function applyMathRules(md: MarkdownIt): MarkdownIt {
return configureMathRules(md, { inline: client.inlineMath.value });
}

const { t } = useI18n();
Expand Down Expand Up @@ -433,8 +437,9 @@ function copyDiff(code: string, idx: number) {
<!-- Non-diff markdown → markstream (smooth streaming + shiki) -->
<MarkdownRender
v-if="seg.kind === 'md'"
:key="`${i}:${client.inlineMath.value ? 1 : 0}`"
:content="seg.text"
:custom-markdown-it="disableInlineMath"
:custom-markdown-it="applyMathRules"
mode="chat"
:code-renderer="renderPlan.codeRenderer"
:is-dark="isDark"
Expand Down
12 changes: 12 additions & 0 deletions apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const props = withDefaults(
uiFontSize?: number;
authReady?: boolean;
conversationToc?: boolean;
/** Inline `$…$` LaTeX rendering in chat markdown (opt-in; can misdetect
prices / env vars / shell paths as math). */
inlineMath?: boolean;
/** Server version from GET /api/v1/meta, shown as a read-only row. */
serverVersion?: string;
/** Available models — used to derive the current model's thinking segments. */
Expand All @@ -63,6 +66,7 @@ const emit = defineEmits<{
setColorScheme: [colorScheme: ColorScheme];
setUiFontSize: [size: number];
setConversationToc: [on: boolean];
setInlineMath: [on: boolean];
login: [];
logout: [];
}>();
Expand Down Expand Up @@ -372,6 +376,14 @@ watch(
<span class="toggle" :class="{ on: conversationToc }" role="switch" :aria-checked="conversationToc" />
</button>

<button type="button" class="srow" @click="emit('setInlineMath', !inlineMath)">
<span class="srow-main">
<span class="srow-label">{{ t('settings.inlineMath') }}</span>
<span class="srow-sub">{{ t('settings.inlineMathHint') }}</span>
</span>
<span class="toggle" :class="{ on: inlineMath }" role="switch" :aria-checked="inlineMath" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the shared Switch for the mobile toggle

For mobile users—especially those using assistive technology—this creates a role="switch" span nested inside a button, so focus lands on a button while the checked state belongs to a different nested widget. It also violates the mandatory apps/kimi-web/AGENTS.md rule: “Use the primitives in src/components/ui/” and “do not hand-roll a bespoke … switch.” Render the setting with components/ui/Switch.vue, as the desktop dialog does, so the interactive element itself owns the switch role, label, checked state, and focus styling.

Useful? React with 👍 / 👎.

</button>

<!-- Account: sign in / out -->
<button v-if="authReady" type="button" class="srow acct out" @click="onLogout">
<span class="srow-main">
Expand Down
15 changes: 15 additions & 0 deletions apps/kimi-web/src/components/settings/SettingsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const props = defineProps<{
sound: boolean;
/** Conversation outline (proportional bubbles, viewport indicator, hover tooltip). */
conversationToc?: boolean;
/** Inline `$…$` LaTeX rendering in chat markdown (opt-in; can misdetect
prices / env vars / shell paths as math). */
inlineMath?: boolean;
/** Global daemon config from GET /api/v1/config. Secrets are redacted server-side. */
config?: AppConfig | null;
/** Models from the daemon catalog, used to label default-model choices. */
Expand All @@ -61,6 +64,7 @@ const emit = defineEmits<{
setNotifyApproval: [on: boolean];
setSound: [on: boolean];
setConversationToc: [on: boolean];
setInlineMath: [on: boolean];
login: [];
logout: [];
openOnboarding: [];
Expand Down Expand Up @@ -397,6 +401,17 @@ function archiveTime(iso: string): string {
@update:model-value="emit('setConversationToc', $event)"
/>
</div>
<div class="row">
<span class="rlabel">
{{ t('settings.inlineMath') }}
<span class="hint">{{ t('settings.inlineMathHint') }}</span>
</span>
<Switch
:model-value="inlineMath ?? false"
:label="t('settings.inlineMath')"
@update:model-value="emit('setInlineMath', $event)"
/>
</div>
</section>

<section class="sec">
Expand Down
30 changes: 30 additions & 0 deletions apps/kimi-web/src/composables/useKimiWebClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,32 @@ function setConversationToc(v: boolean): void {
saveConversationTocToStorage(v);
}

// ---------------------------------------------------------------------------
// Inline `$…$` LaTeX rendering in chat markdown. Off by default: the single-`$`
// inline rule can misdetect prices / env vars / shell paths ($5, $PATH,
// $HOME/bin) as math, so users opt in via Settings. Persisted per browser.
// ---------------------------------------------------------------------------
const INLINE_MATH_STORAGE_KEY = STORAGE_KEYS.inlineMath;
function loadInlineMathFromStorage(): boolean {
try {
return safeGetString(INLINE_MATH_STORAGE_KEY) === 'true';
} catch {
return false;
}
}
function saveInlineMathToStorage(v: boolean): void {
try {
safeSetString(INLINE_MATH_STORAGE_KEY, v ? 'true' : 'false');
} catch {
// ignore
}
}
const inlineMath = ref<boolean>(loadInlineMathFromStorage());
function setInlineMath(v: boolean): void {
inlineMath.value = v;
saveInlineMathToStorage(v);
}

// ---------------------------------------------------------------------------
// Onboarding: a "has the user been onboarded" flag that gates the first-run
// onboarding screen (preference: language). Persisted; can be reset to re-open
Expand Down Expand Up @@ -2889,6 +2915,10 @@ export function useKimiWebClient() {
conversationToc,
setConversationToc,

// Inline `$…$` LaTeX rendering (opt-in)
inlineMath,
setInlineMath,

// Color scheme
colorScheme: appearance.colorScheme,
setColorScheme: appearance.setColorScheme,
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-web/src/i18n/locales/en/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export default {
exportLogBtn: 'Export log',
conversationToc: 'Show conversation outline',
conversationTocHint: 'Show a clickable outline in the right margin to jump between messages',
inlineMath: 'Inline math rendering',
inlineMathHint: 'Render $…$ as inline LaTeX in messages; may misfire on prices or variables like $PATH',
archivedTitle: 'Archived sessions',
archivedDesc: 'Browse archived sessions, see their workspace path, name, and archive time, and restore them to the session list.',
archivedSearch: 'Search archived sessions',
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-web/src/i18n/locales/zh/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export default {
exportLogBtn: '导出日志',
conversationToc: '显示对话目录',
conversationTocHint: '在右侧显示可点击跳转的对话目录',
inlineMath: '行内公式渲染',
inlineMathHint: '将消息中的 $…$ 渲染为行内 LaTeX 公式,价格或 $PATH 等变量可能被误识别',
archivedTitle: '已归档会话',
archivedDesc: '查看已归档会话,确认其所属工作区路径、会话名称和归档时间,并可恢复到会话列表。',
archivedSearch: '搜索已归档会话',
Expand Down
25 changes: 25 additions & 0 deletions apps/kimi-web/src/lib/markdownMath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// apps/kimi-web/src/lib/markdownMath.ts
// Markdown math-rule policy for chat rendering.
//
// `$$…$$` display math (the `math_block` rule) is always left enabled.
// The single-`$` inline `math` rule is OPT-IN: bare `$` appears constantly in
// prices, env vars, and shell paths (`$5`, `$PATH`, `$HOME/bin`), and the rule
// will misdetect those as math and swallow them into a broken formula. Users
// who want inline `$…$` LaTeX enable it in Settings; everyone else keeps
// literal `$` text. The toggle is consumed in components/chat/Markdown.vue.

import type { MarkdownIt } from 'markstream-vue';

export interface MathRuleOptions {
/** Render `$…$` spans as inline KaTeX. See the misdetection note above. */
inline: boolean;
}

export function configureMathRules(md: MarkdownIt, opts: MathRuleOptions): MarkdownIt {
if (opts.inline) {
md.inline.ruler.enable('math');
} else {
md.inline.ruler.disable('math');
}
return md;
}
4 changes: 4 additions & 0 deletions apps/kimi-web/src/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export const STORAGE_KEYS = {
// users who explicitly turned it off while it was experimental keep their
// preference after it became on-by-default.
conversationToc: 'kimi-web.beta-toc',
// Inline `$…$` LaTeX rendering in chat markdown. Off by default — the
// single-`$` rule can misdetect prices, env vars, and shell paths
// ($5, $PATH, $HOME/bin) as math, so users opt in via Settings.
inlineMath: 'kimi-web.inline-math',
notifyOnComplete: 'kimi-web.notify-on-complete',
notifyOnQuestion: 'kimi-web.notify-on-question',
notifyOnApproval: 'kimi-web.notify-on-approval',
Expand Down
38 changes: 38 additions & 0 deletions apps/kimi-web/test/lib-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
parseFilePathLinkCandidate,
} from '../src/lib/filePathLinks';
import { parseDiff } from '../src/lib/parseDiff';
import { configureMathRules } from '../src/lib/markdownMath';
import { buildDiffLines } from '../src/lib/diffLines';
import { buildEditDiffLines } from '../src/lib/toolDiff';
import { createCoalescedAsyncRunner } from '../src/lib/snapshotSync';
Expand Down Expand Up @@ -863,3 +864,40 @@ describe('keepLiveSubagents', () => {
expect(merged?.outputBytes).toBe(200);
});
});

describe('configureMathRules', () => {
// Minimal markdown-it stand-in: records ruler enable/disable calls.
function fakeMd() {
const calls: { op: 'enable' | 'disable'; name: string }[] = [];
const md = {
inline: {
ruler: {
disable: (name: string) => {
calls.push({ op: 'disable' as const, name });
return md;
},
enable: (name: string) => {
calls.push({ op: 'enable' as const, name });
return md;
},
},
},
};
return { md, calls };
}

it('disables the inline `$…$` math rule when inline math is off (default)', () => {
const { md, calls } = fakeMd();
// Prices, env vars, and shell paths ($5, $PATH, $HOME/bin) must stay
// literal, so the default path disables the single-$ inline rule.
const out = configureMathRules(md as never, { inline: false });
expect(out).toBe(md);
expect(calls).toEqual([{ op: 'disable', name: 'math' }]);
});

it('enables the inline `$…$` math rule when the user opts in', () => {
const { md, calls } = fakeMd();
configureMathRules(md as never, { inline: true });
expect(calls).toEqual([{ op: 'enable', name: 'math' }]);
});
});
Loading