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-bound-dom-growth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Bound DOM growth in long sessions — tail-biased truncation with expand-to-full for long tool outputs, collapsible tool bodies unmount when closed, tool groups default to collapsed (auto-open while running), and lighter scroll-follow hot paths.
14 changes: 12 additions & 2 deletions apps/kimi-web/src/components/chat/ConversationPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,14 @@ const scrollKey = computed<ScrollKey>(() => {
const thinkingLen = last?.thinking?.length ?? 0;
const toolsLen =
last?.tools?.reduce(
(n, tool) => n + tool.name.length + (tool.arg?.length ?? 0) + (tool.output?.join('').length ?? 0),
// Track output growth via line count + tail-line length instead of
// joining every tool output on each change.
(n, tool) =>
n +
tool.name.length +
(tool.arg?.length ?? 0) +
(tool.output?.length ?? 0) +
(tool.output?.at(-1)?.length ?? 0),
0,
) ?? 0;
return {
Expand Down Expand Up @@ -1126,7 +1133,10 @@ function rebindScrollObservers(): void {
updatePanesScrollbarWidth();
if (contentObserver) {
contentObserver.disconnect();
if (el) contentObserver.observe(el, { childList: true, subtree: true, characterData: true });
// Drop characterData: per-token text updates during streaming fired
// this observer at delta rate; structural changes are enough, and the
// scroll follow is driven by the reactive scrollKey watcher anyway.
if (el) contentObserver.observe(el, { childList: true, subtree: true });
}
if (resizeObserver) {
resizeObserver.disconnect();
Expand Down
18 changes: 14 additions & 4 deletions apps/kimi-web/src/components/chat/ToolGroup.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- apps/kimi-web/src/components/chat/ToolGroup.vue -->
<script setup lang="ts">
import { computed, inject, nextTick, ref } from 'vue';
import { computed, inject, nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ToolCall from './ToolCall.vue';
import { toolStackKey, toolStackPosition } from '../chatTurnRendering';
Expand All @@ -25,14 +25,23 @@ const emit = defineEmits<{
openAgent: [toolCallId: string];
}>();

const open = ref(true);

const count = computed(() => props.tools.length);
const aggregateStatus = computed<'running' | 'error' | 'done'>(() => {
if (props.tools.some((t) => t.tool.status === 'running')) return 'running';
if (props.tools.some((t) => t.tool.status === 'error')) return 'error';
return 'done';
});

// kimi-ui: groups default to collapsed (they used to default open, mounting
// every tool call body of every past turn). A group with tools still running
// opens itself so live execution stays visible.
const open = ref(aggregateStatus.value === 'running');

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 Ensure tool groups reopen when tools start running

When an existing tool stack is initially collapsed (for example after refreshing a long session with completed tools) and a later running tool is appended to the same assistant turn, aggregateStatus updates to running but open keeps the one-time initial value from mount. Because the group body is now v-if="open", the live tool rows stay unmounted behind a collapsed header even though the new behavior intends running groups to open automatically; add a watcher/effect that sets open when the aggregate status becomes running.

Useful? React with 👍 / 👎.

// A collapsed group must also reopen when a running tool is appended LATER
// (e.g. a completed stack from a refresh gains a new in-flight tool) — the
// initial ref only captures mount-time state.
watch(aggregateStatus, (status) => {
if (status === 'running') open.value = true;
});
const { t } = useI18n();

const statusLabel = computed(() => {
Expand Down Expand Up @@ -70,7 +79,8 @@ function onHeadClick(): void {
<Icon class="tg-car" name="chevron-right" size="sm" />
</button>
<div class="tool-group-body" :class="{ open }" :inert="!open">
<div class="tool-group-body-inner">
<!-- kimi-ui: unmount when collapsed (same DOM-bloat fix as ToolRow). -->
<div v-if="open" class="tool-group-body-inner">
<ToolCall
v-for="(item, si) in tools"
:key="toolStackKey(item)"
Expand Down
4 changes: 3 additions & 1 deletion apps/kimi-web/src/components/chat/ToolRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ function onHeadClick(): void {
<Icon v-if="expandable" class="car" :name="open ? 'chevron-down' : 'chevron-right'" size="sm" />
</div>
<div class="bb" :class="{ open }" :inert="!open">
<div class="bb-pad">
<!-- kimi-ui: unmount the body when closed — collapsed tool rows used to
keep every output line in the DOM (grid 0fr only hid them visually). -->
<div v-if="open" class="bb-pad">
<slot />
</div>
</div>
Expand Down
42 changes: 38 additions & 4 deletions apps/kimi-web/src/components/chat/tool-calls/ToolOutputBlock.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
<!-- Shared line-oriented tool output block. Keeps long outputs to a readable
viewport while preserving the tool card's normal typography. -->
viewport while preserving the tool card's normal typography.
kimi-ui: outputs beyond TRUNCATE_LINE_COUNT render tail-biased (the last
N lines, where errors/results live) behind an expand button, so huge logs
no longer put thousands of divs in the DOM. -->
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';

const OUTPUT_SCROLL_LINE_COUNT = 50;
// Default render window for long outputs (tail-biased).
const TRUNCATE_LINE_COUNT = 80;

const props = defineProps<{
lines?: string[];
emptyText?: string;
}>();

const { t } = useI18n();
const expanded = ref(false);

const outputLines = computed(() => props.lines ?? []);
const isScrollable = computed(() => outputLines.value.length > OUTPUT_SCROLL_LINE_COUNT);
const truncated = computed(
() => !expanded.value && outputLines.value.length > TRUNCATE_LINE_COUNT,
);
const visibleLines = computed(() =>
truncated.value ? outputLines.value.slice(-TRUNCATE_LINE_COUNT) : outputLines.value,
);
const isScrollable = computed(() => visibleLines.value.length > OUTPUT_SCROLL_LINE_COUNT);
const outputStyle = { '--tool-output-visible-lines': String(OUTPUT_SCROLL_LINE_COUNT) };
</script>

<template>
<div class="bb-code tool-output-block" :class="{ scroll: isScrollable }" :style="outputStyle">
<div v-if="outputLines.length === 0 && emptyText" class="bb-empty">{{ emptyText }}</div>
<div v-for="(line, i) in outputLines" :key="i">{{ line }}</div>
<button v-if="truncated" class="truncate-toggle" type="button" @click="expanded = true">
{{ t('tools.output.showAll', { count: outputLines.length }) }}
</button>
<div v-for="(line, i) in visibleLines" :key="i">{{ line }}</div>
</div>
</template>

Expand All @@ -39,4 +57,20 @@ const outputStyle = { '--tool-output-visible-lines': String(OUTPUT_SCROLL_LINE_C
color: var(--color-text-muted);
font-style: italic;
}
.truncate-toggle {
display: block;
width: 100%;
margin: 0 0 var(--space-2);
padding: var(--space-1) 0;
border: none;
border-bottom: 1px dashed var(--color-line);
background: transparent;
color: var(--color-accent);
font: var(--text-xs) var(--font-ui);
cursor: pointer;
text-align: center;
}
.truncate-toggle:hover {
text-decoration: underline;
}
</style>
3 changes: 3 additions & 0 deletions apps/kimi-web/src/i18n/locales/en/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export default {
error: 'failed',
done: 'done',
},
output: {
showAll: 'Show all {count} lines',
},
ask: {
dismissed: 'Dismissed',
answer: '{count} answer',
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-web/src/i18n/locales/zh/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export default {
error: '有失败',
done: '已完成',
},
output: {
showAll: '显示全部 {count} 行',
},
ask: {
dismissed: '已忽略',
answer: '{count} 个回答',
Expand Down