Skip to content
5 changes: 5 additions & 0 deletions .changeset/web-code-block-line-numbers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Fix code block line numbers drifting out of sync with the code lines in plain-text code blocks.
5 changes: 5 additions & 0 deletions .changeset/web-ctrl-enter-steer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Change the steer-into-running-turn shortcut to Ctrl+Enter / Cmd+Enter; the previously advertised Ctrl+S could open the browser's Save Page dialog.
5 changes: 5 additions & 0 deletions .changeset/web-full-width-chat-column.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Let the chat column follow the window width instead of capping it at a fixed reading width, removing the wide empty margins on large screens.
13 changes: 7 additions & 6 deletions apps/kimi-web/src/components/chat/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -491,12 +491,13 @@ function handleKeydown(e: KeyboardEvent): void {
}
}

// Ctrl+S / Cmd+S — steer into the running turn (TUI parity)
if (e.key === 's' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
if (props.running) {
e.preventDefault();
handleSteer();
}
// Ctrl+Enter / Cmd+Enter — steer into the running turn; falls through to the
// normal submit path when idle. (The composer previously advertised Ctrl+S,
// but Chrome Canary intercepts it for the Save Page dialog before the page
// can preventDefault it.)
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && props.running) {
e.preventDefault();
handleSteer();
return;
}

Expand Down
15 changes: 9 additions & 6 deletions apps/kimi-web/src/components/chat/ConversationPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ function updateActiveTocQuery(): void {

// --- TOC occlusion by wide tables -------------------------------------------
// Wide markdown tables (up to --p-table-max) can extend past the TOC rail,
// which stays anchored to the reading-column edge. While a table actually
// which stays anchored to the pane's right edge. While a table actually
// covers the rail we hide the TOC temporarily so the table stays fully
// interactive (clicks, text selection, horizontal scroll). The user's TOC
// setting is untouched and the rail returns as soon as the table scrolls away.
Expand All @@ -354,7 +354,7 @@ function updateTocTableOcclusion(): void {
? pane.closest('.con')?.querySelector<HTMLElement>('.conversation-toc')
: null;
// The hit x is the centre of the fixed rail bar: `.toc-bar` keeps a stable x
// even when hover expands the labels rightward, so hovering the TOC itself
// even when hover expands the labels leftward, so hovering the TOC itself
// never flips the state (the nav centre would).
const bar = toc?.querySelector<HTMLElement>('.toc-bar');
let covered = false;
Expand Down Expand Up @@ -423,7 +423,9 @@ const panesRef = ref<HTMLElement | null>(null);
const dockRef = ref<HTMLElement | null>(null);
const panesScrollbarWidth = ref(0);
const dockHeight = ref(0);
const chatDockStyle = computed(() => ({
// Exposed on `.con` (not the dock alone) so the TOC rail can compensate for
// the panes scrollbar gutter at the pane's right edge, just like the dock does.
const conStyle = computed(() => ({
'--panes-scrollbar-width': `${panesScrollbarWidth.value}px`,
}));
type ComposerHandle = {
Expand Down Expand Up @@ -1251,7 +1253,7 @@ defineExpose({ loadComposerForEdit, focusComposer });
</script>

<template>
<section class="con" :class="{ mobile }">
<section class="con" :class="{ mobile }" :style="conStyle">
<!-- Chat context header: workspace/session, git status, open-in-editor,
copy-all, PR. Hidden for the empty-composer (no session context yet). -->
<ChatHeader
Expand Down Expand Up @@ -1443,7 +1445,6 @@ defineExpose({ loadComposerForEdit, focusComposer });
<ChatDock
v-if="!(turns.length === 0 && !sessionLoading)"
:ref="bindChatDock"
:style="chatDockStyle"
:session-id="sessionId"
:running="running"
:starting="starting"
Expand Down Expand Up @@ -1530,7 +1531,9 @@ defineExpose({ loadComposerForEdit, focusComposer });

<style scoped>
.con {
--read-max: 760px;
/* The reading column follows the pane width — no fixed max-width cap, so
wide windows don't leave empty margins on both sides. */
--read-max: 100%;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep regular chat content capped by the reading token

As committed, --read-max: 100% makes regular assistant prose and user bubbles expand to 94%/78% of whatever desktop pane width is available, while the web design-system view still defines --p-content-max as the chat reading-column max width and states that regular chat prose stays within that 760px column. On large monitors this regresses readability and also bypasses the tokenized layout contract; if full-width prose is intended, the design-system contract should be updated together, otherwise keep the normal stream capped and reserve wider growth for the existing table/content exceptions.

AGENTS.md reference: apps/kimi-web/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — at that commit the design-system contract indeed lagged behind the change. It has been updated since (6edc9c0): --p-content-max no longer documents the chat reading column (it now only backs the xl dialog width), and the design-system view's chat/wide-tables sections were rewritten to state that the chat column follows the pane width with no fixed cap, including the outline rail's new anchor at the pane's right edge. Full-width prose is the deliberate behavior change of this PR — the readability trade-off on very wide monitors is accepted on purpose.

display: flex;
flex-direction: column;
min-width: 0;
Expand Down
79 changes: 37 additions & 42 deletions apps/kimi-web/src/components/chat/ConversationToc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ const emit = defineEmits<{

const { t } = useI18n();

// Width the rail needs beside the reading column once its labels are fully
// revealed on hover/focus: 3px bar + 10px gap + 220px label, plus a small
// buffer so the text never kisses the container edge. Kept in sync with the
// `.toc-bar` / `.toc-label` rules below.
// Width the rail needs to its left once its labels are fully revealed on
// hover/focus: 3px bar + 10px gap + 220px label, plus a small buffer so the
// text never kisses the container edge. Kept in sync with the `.toc-bar` /
// `.toc-label` rules below.
const EXPANDED_WIDTH = 240;

const navRef = ref<HTMLElement | null>(null);
// Whether the rail, once expanded, fits within the room to the right of the
// reading column. When it would overflow, we hide the outline entirely rather
// than showing a panel that gets clipped by the container edge.
// Whether the rail, once expanded, fits within the room to its left (the
// labels reveal leftward over the content). When it would overflow, we hide
// the outline entirely rather than showing a panel that gets clipped by the
// container edge.
const fits = ref(true);

let observer: ResizeObserver | null = null;
Expand All @@ -47,9 +48,9 @@ function measure(): void {
const nav = navRef.value;
const parent = nav?.offsetParent as HTMLElement | null;
if (!nav || !parent) return;
const navLeft = nav.getBoundingClientRect().left;
const parentRight = parent.getBoundingClientRect().right;
fits.value = parentRight - navLeft >= EXPANDED_WIDTH;
const navRight = nav.getBoundingClientRect().right;
const parentLeft = parent.getBoundingClientRect().left;
fits.value = navRight - parentLeft >= EXPANDED_WIDTH;
}

// The outline is only useful once there is something to navigate, and it never
Expand Down Expand Up @@ -93,8 +94,9 @@ onBeforeUnmount(() => {

<template>
<!-- Conversation outline: a vertical list of short bars (one per user query),
vertically centered beside the chat. Hovering the list enlarges the bars
and reveals each query's title to the right, making rows easy to click. -->
vertically centered at the pane's right edge. Hovering the list enlarges
the bars and reveals each query's title to the left, over the content,
making rows easy to click. -->
<nav
v-if="visible"
ref="navRef"
Expand Down Expand Up @@ -125,38 +127,18 @@ onBeforeUnmount(() => {
z-index: var(--z-sticky);
top: 50%;
transform: translateY(-50%);
/* Anchor to the reading-column edge, the rail's original position. Tables
that grow past it (up to --p-table-max) temporarily hide the rail via the
occlusion hit-test in ConversationPane, so proximity is safe again.
The cqi cap keeps the rail inside narrow containers. */
--toc-content-max: min(
var(--p-content-max),
calc(100cqi - var(--space-5) - var(--space-5))
);
left: calc(50% + (var(--toc-content-max) / 2) + 14px);
/* Anchor to the pane's right edge, just inside the panes scrollbar gutter.
The reading column spans the full pane width, so that edge is the only
stable outside position left; labels reveal leftward over the content.
Tables that reach the rail temporarily hide it via the occlusion hit-test
in ConversationPane, so the overlap is safe. */
right: calc(var(--panes-scrollbar-width, 0px) + var(--space-2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid placing the TOC hover bridge over messages

With the rail now anchored inside the pane edge, its existing invisible hover bridge (left: -14px plus the right-side extension) sits over the right edge of full-width chat content; for example right-aligned user bubbles end near the pane edge, so clicks or text selection in the vertical band around the TOC are intercepted by the nav instead of the message. This only happens when the TOC is enabled and a message crosses the rail’s vertical range, but it makes ordinary chat content non-interactive there; either keep the rail outside the content, reserve padding, or make only the actual TOC rows receive pointer events.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in d78caaf: the hover bridge is removed entirely — only the actual outline rows receive pointer events now, so clicks and text selection along the pane's right edge reach the messages underneath. The expanded rows are wide enough targets once the panel is open, so the bridge was no longer pulling its weight.

display: flex;
flex-direction: column;
justify-content: center;
opacity: 0.5;
transition: opacity var(--duration-base) var(--ease-out);
}
/* Invisible hover bridge: the collapsed rail is only a few px wide, so this
extends the hover target on both sides to make the outline easy to open and
forgiving to stay within. The left side covers only the 14px gap to the
content edge — a table wide enough to reach past the gap also covers the
bar, which hides the rail (pointer-events: none) before the bridge can
steal its events. Kept at z-index 0 so it sits behind the rows (which are
raised to z-index 1) — otherwise the bridge, as a positioned pseudo-element,
paints above the in-flow rows and swallows their clicks. */
.conversation-toc::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: -14px;
right: -48px;
z-index: 0;
}
.conversation-toc:hover,
.conversation-toc:focus-within { opacity: 1; }

Expand All @@ -175,6 +157,8 @@ onBeforeUnmount(() => {

.toc-row {
display: flex;
/* Bar rightmost (its x stays pinned at the pane edge), label extends left. */
flex-direction: row-reverse;
align-items: center;
gap: 10px;
height: 18px;
Expand Down Expand Up @@ -213,20 +197,31 @@ onBeforeUnmount(() => {
color var(--duration-fast) var(--ease-out);
}

/* Hover / focus: enlarge bars and reveal labels to the right. */
/* Hover / focus: enlarge bars and reveal labels to the left. */
.conversation-toc:hover .toc-bar,
.conversation-toc:focus-within .toc-bar { height: 18px; opacity: 0.5; }
.conversation-toc:hover .toc-label,
.conversation-toc:focus-within .toc-label { max-width: 220px; opacity: 1; }
/* The revealed labels float over the message content, so the expanded outline
gets its own panel background to stay readable. The padding grows leftward
only, keeping the rail bar's x stable (the table-occlusion hit-test in
ConversationPane depends on it). */
.conversation-toc:hover .toc-scroll,
.conversation-toc:focus-within .toc-scroll {
padding-left: var(--space-3);
border-radius: var(--radius-md);
background: var(--color-bg);
box-shadow: var(--shadow-md);
}

.toc-row.active .toc-bar { opacity: 1; height: 18px; }
.toc-row.active .toc-label { color: var(--color-accent); font-weight: var(--weight-medium); }
.toc-row:hover .toc-bar { opacity: 1; }
.toc-row:hover .toc-label { color: var(--color-text); }

/* When there is not enough room to the right of the reading column to reveal
the labels, the rail is kept mounted (so its position can keep being
measured) but hidden from view and from pointer/screen-reader interaction. */
/* When there is not enough room to the left of the rail to reveal the labels,
the rail is kept mounted (so its position can keep being measured) but
hidden from view and from pointer/screen-reader interaction. */
.conversation-toc.toc-clipped {
visibility: hidden;
pointer-events: none;
Expand Down
6 changes: 5 additions & 1 deletion apps/kimi-web/src/components/chat/Markdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,11 @@ function copyDiff(code: string, idx: number) {
overflow-x: auto;
font: var(--text-sm)/1.65 var(--font-mono);
}
.md :deep(.code-block-container pre code) {
.md :deep(.code-block-container pre code),
/* Bare markstream fallback pres (rendered without .code-block-container) nest
<code> deeper than `pre > code`, so the inline-chip rule above would shrink
it and desync the line-number overlay from the code lines. */
.md :deep(pre[data-markstream-pre] code) {
font: inherit;
color: var(--color-text);
background: none;
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-web/src/i18n/locales/en/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export default {
placeholder: 'Type a message…',
send: 'Send ↵',
queueLabel: 'Queue',
placeholderRunning: 'Press Enter to queue · Ctrl+S to inject into the running turn',
placeholderRunning: 'Press Enter to queue · Ctrl+Enter to inject into the running turn',
starting: 'Sending…',
queueAutoDrain: 'sends automatically when the current turn ends',
queueNext: 'Up next',
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-web/src/i18n/locales/zh/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export default {
placeholder: '输入消息…',
send: '发送 ↵',
queueLabel: '队列',
placeholderRunning: '输入会加入队列 · Ctrl+S 立即插入运行中的回合',
placeholderRunning: '输入会加入队列 · Ctrl+Enter 立即插入运行中的回合',
starting: '正在发送…',
queueAutoDrain: '当前回合结束后自动逐条发送',
queueNext: '下一条',
Expand Down
10 changes: 5 additions & 5 deletions apps/kimi-web/src/views/DesignSystemView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ onUnmounted(() => {
<thead><tr><th>Token</th><th>Value</th><th>Usage</th></tr></thead>
<tbody>
<tr><td class="tk">--p-sidebar-w</td><td class="val">264px</td><td>left session sidebar width</td></tr>
<tr><td class="tk">--p-content-max</td><td class="val">760px</td><td>chat reading-column max width (regular chat prose)</td></tr>
<tr><td class="tk">--p-content-max</td><td class="val">760px</td><td>xl dialog width</td></tr>
<tr><td class="tk">--p-content-wide</td><td class="val">920px</td><td>wide content (settings / panel)</td></tr>
<tr><td class="tk">--p-table-max</td><td class="val">1040px</td><td>desktop wide-table max width (see §04)</td></tr>
<tr><td class="tk">--p-table-cell-max</td><td class="val">700px</td><td>max width of a single table column; longer cell content wraps (see §04)</td></tr>
Expand Down Expand Up @@ -1028,7 +1028,7 @@ onUnmounted(() => {

<h3 class="sub">Unified message stream</h3>
<div class="stage-wrap">
<div class="stage-bar"><span class="st">Conversation · 760px reading column</span></div>
<div class="stage-bar"><span class="st">Conversation · full-width column</span></div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Make the design-system demo match full-width chat

Fresh evidence since the earlier review is that this renamed stage now says “full-width column”, but the demo rendered immediately below is still .demo-chat with max-width: 560px, so the canonical design-system overlay continues to show a capped chat column even though the product layout was changed to full width. Future visual checks against this spec can therefore approve the old narrow layout; update this demo’s cap (or scope it away from the chat stream example) so §04 reflects the new behavior.

AGENTS.md reference: apps/kimi-web/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in d78caaf: dropped the 560px cap from .demo-chat, so the §04 demo renders the chat stream at the full stage width, matching the renamed "full-width column" stage.

<div class="stage p col" style="align-items:center;background:#fff">
<div class="demo-chat">

Expand Down Expand Up @@ -1122,7 +1122,7 @@ onUnmounted(() => {
</div>
</div>
</div>
<p><b>Wide markdown tables (desktop):</b> regular chat prose stays within the 760px reading column (<code>--p-content-max</code>). On desktop a wide table may grow naturally with its content up to 1040px (<code>--p-table-max</code>), centred within the conversation pane; beyond that the excess scrolls horizontally inside the table's own wrapper — the page and the chat area never scroll sideways. A single column is capped at 700px (<code>--p-table-cell-max</code>), so long cell content wraps inside the cell instead of stretching the table. The conversation outline (TOC) keeps its usual position just outside the reading column; when a table grows past it and scrolls under the rail, the TOC is hidden temporarily and returns as soon as the table leaves, without touching the user's TOC setting. On mobile a table never breaks out of the reading column.</p>
<p><b>Wide markdown tables (desktop):</b> the chat reading column follows the pane width — there is no fixed max-width cap. On desktop a wide table may grow naturally with its content up to 1040px (<code>--p-table-max</code>), centred within the conversation pane; beyond that the excess scrolls horizontally inside the table's own wrapper — the page and the chat area never scroll sideways. A single column is capped at 700px (<code>--p-table-cell-max</code>), so long cell content wraps inside the cell instead of stretching the table. The conversation outline (TOC) is anchored to the pane's right edge, just inside the scrollbar gutter, and reveals its labels leftward over the content on hover; when a table grows past it and scrolls under the rail, the TOC is hidden temporarily and returns as soon as the table leaves, without touching the user's TOC setting. On mobile a table never breaks out of the column.</p>

<h3 class="sub">Tool calls: compact by default, grouped, expand on demand</h3>
<p>High-frequency calls like <code>read_file</code> / <code>bash</code> / <code>grep</code> are "operational noise" — if each one took a full card, parallel triggers would quickly drown out the conversation.
Expand Down Expand Up @@ -1206,7 +1206,7 @@ onUnmounted(() => {
<h3 class="sub">Responsive</h3>
<p>See §02 <code>--p-bp-sm</code> for the breakpoint. This section only gives mobile-adaptation pointers for the chat interface; a full mobile mockup is out of scope for this spec.</p>
<div class="callout info"><span class="ico">i</span><div>
At ≤640px: dialogs anchor to the bottom as Sheets (xl top radius, top drag handle), the sidebar collapses into an expandable drawer, the Composer toolbar is allowed to wrap, and the chat reading column drops its max-width to fill the screen.
At ≤640px: dialogs anchor to the bottom as Sheets (xl top radius, top drag handle), the sidebar collapses into an expandable drawer, and the Composer toolbar is allowed to wrap.
</div></div>
</section>

Expand Down Expand Up @@ -2313,7 +2313,7 @@ onUnmounted(() => {
.demo-stack { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.demo-col { display: flex; flex-direction: column; gap: 10px; }
.demo-grow { flex: 1; min-width: 0; }
.demo-chat { display: flex; flex-direction: column; gap: 14px; width: 100%; max-width: 560px; }
.demo-chat { display: flex; flex-direction: column; gap: 14px; width: 100%; }

/* Icon catalog (§02 Icon library) */
.icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 8px; margin: 14px 0; }
Expand Down