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

Hide the conversation outline when there is not enough room to expand its labels, so it no longer clips against the window edge.
70 changes: 66 additions & 4 deletions apps/kimi-web/src/components/chat/ConversationToc.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- apps/kimi-web/src/components/chat/ConversationToc.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type { ChatTurn } from '../../types';

Expand All @@ -25,11 +25,66 @@ 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.
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.
const fits = ref(true);

let observer: ResizeObserver | null = null;

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;
}

// The outline is only useful once there is something to navigate, and it never
// shows on mobile or while the session is still loading.
// shows on mobile or while the session is still loading. `fits` is kept out of
// this computed so the nav stays mounted (and measurable) even when hidden;
// clipping is applied via the `toc-clipped` class instead.
const visible = computed(
() => !props.mobile && !props.sessionLoading && props.items.length > 1,
);

// The nav is rendered only while `visible` (v-if), so a mount while navRef is
// still null (during sessionLoading, on mobile, or before a second user turn)
// would skip the ResizeObserver setup and leave `fits` at its default `true`.
// Re-initialize whenever the nav is actually rendered so `fits` is measured
// against the real layout instead.
watch(
visible,
(isVisible) => {
observer?.disconnect();
observer = null;
if (!isVisible) return;
void nextTick(() => {
const nav = navRef.value;
const parent = nav?.offsetParent as HTMLElement | null;
if (!nav || !parent) return;
if (typeof ResizeObserver !== 'undefined') {
observer = new ResizeObserver(measure);
observer.observe(parent);
}
measure();
});
},
{ immediate: true },
);

onBeforeUnmount(() => {
observer?.disconnect();
observer = null;
});
</script>

<template>
Expand All @@ -38,8 +93,11 @@ const visible = computed(
and reveals each query's title to the right, making rows easy to click. -->
<nav
v-if="visible"
ref="navRef"
class="conversation-toc"
:class="{ 'toc-clipped': !fits }"
:aria-label="t('conversation.toc')"
:aria-hidden="fits ? undefined : true"
>
<div class="toc-scroll">
<button
Expand Down Expand Up @@ -151,7 +209,11 @@ const visible = computed(
.toc-row:hover .toc-bar { opacity: 1; }
.toc-row:hover .toc-label { color: var(--color-text); }

@container (max-width: 920px) {
.conversation-toc { display: none; }
/* 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. */
.conversation-toc.toc-clipped {
visibility: hidden;
pointer-events: none;
}
</style>
Loading