-
Notifications
You must be signed in to change notification settings - Fork 897
feat(web): remove sidebar and panel max-width limits #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f7695d9
cffcde3
26e1cd5
fa2ccb3
4554233
f98d168
eac41eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": minor | ||
| --- | ||
|
|
||
| Allow the web sidebar and detail panel to be resized up to the available viewport width, keeping their resize handles reachable on narrow windows. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,22 +115,6 @@ function onGlobalKeydown(e: KeyboardEvent): void { | |
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Layout: resizable session column. ResizeHandle owns the column width (with | ||
| // localStorage persistence); we mirror it here to drive the App grid. | ||
| // --------------------------------------------------------------------------- | ||
| const { | ||
| SIDEBAR_WIDTH_KEY, | ||
| SIDEBAR_DEFAULT, | ||
| SIDEBAR_MIN, | ||
| SIDEBAR_MAX, | ||
| sessionColWidth, | ||
| sidebarCollapsed, | ||
| sideWidth, | ||
| loadSidebarCollapsed, | ||
| toggleSidebarCollapse, | ||
| } = useSidebarLayout(); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Unified right-side detail layer. Only one detail is open at a time. The | ||
| // shared `detailTarget` ref lives here so the file-preview and detail-panel | ||
|
|
@@ -152,6 +136,28 @@ const { | |
| revealPreviewFile, | ||
| } = useFilePreview({ client, detailTarget }); | ||
|
|
||
| // True while the right-side slot is actually occupied, so the sidebar reserves | ||
| // room for it and the conversation can never be squeezed. Keyed off detailTarget | ||
| // (the real occupant) rather than previewTarget, which can stay set after the | ||
| // panel is hidden. | ||
| const previewOpen = computed(() => detailTarget.value !== null); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Layout: resizable session column. ResizeHandle owns the column width (with | ||
| // localStorage persistence); we mirror it here to drive the App grid. | ||
| // --------------------------------------------------------------------------- | ||
| const { | ||
| SIDEBAR_WIDTH_KEY, | ||
| SIDEBAR_DEFAULT, | ||
| SIDEBAR_MIN, | ||
| sidebarMax, | ||
| sessionColWidth, | ||
| sidebarCollapsed, | ||
| sideWidth, | ||
| loadSidebarCollapsed, | ||
| toggleSidebarCollapse, | ||
| } = useSidebarLayout({ previewOpen }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Unified right-side detail layer (thinking / compaction / agent / diff / side | ||
| // chat) plus the preview-panel width. Only one detail is open at a time. | ||
|
|
@@ -160,8 +166,9 @@ const { | |
| PREVIEW_WIDTH_KEY, | ||
| PREVIEW_MIN, | ||
| previewDefaultWidth, | ||
| previewMaxWidth, | ||
| previewMax, | ||
| previewWidth, | ||
| previewPanelWidth, | ||
| thinkingPanelText, | ||
| thinkingVisible, | ||
| openThinkingPanel, | ||
|
|
@@ -359,7 +366,9 @@ function handleCommand(cmd: string): void { | |
| if (cmd === '/btw' || cmd.startsWith('/btw ')) { | ||
| const arg = cmd.slice('/btw'.length).trim(); | ||
| if (!arg && client.sideChatVisible.value) { | ||
| client.closeSideChat(); | ||
| // Use the detail-layer close so detailTarget is cleared too; the bare | ||
| // client.closeSideChat() only hides the panel and leaves detailTarget set. | ||
| closeSideChat(); | ||
| } else { | ||
| void openSideChatTab(arg || undefined); | ||
| } | ||
|
|
@@ -527,13 +536,13 @@ function openPr(url: string): void { | |
| v-else | ||
| class="app" | ||
| :class="{ mobile: isMobile, 'sidebar-collapsed': sidebarCollapsed && !isMobile }" | ||
| :style="{ '--side-w': sideWidth + 'px', '--preview-w': previewWidth + 'px' }" | ||
| :style="{ '--side-w': sideWidth + 'px', '--preview-w': previewPanelWidth + 'px' }" | ||
| > | ||
| <!-- Desktop navigation: workspace rail + resizable session column. --> | ||
| <template v-if="!isMobile"> | ||
| <Sidebar | ||
| v-show="!sidebarCollapsed" | ||
| :col-width="sessionColWidth" | ||
| :col-width="sideWidth" | ||
| :active-workspace="client.visibleWorkspace.value" | ||
| :active-workspace-id="client.activeWorkspaceId.value" | ||
| :sessions="client.sessionsForView.value" | ||
|
|
@@ -561,7 +570,7 @@ function openPr(url: string): void { | |
| :storage-key="SIDEBAR_WIDTH_KEY" | ||
| :default-width="SIDEBAR_DEFAULT" | ||
| :min="SIDEBAR_MIN" | ||
| :max="SIDEBAR_MAX" | ||
| :max="sidebarMax" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the desktop window grows after the sidebar handle has mounted, this viewport-derived Useful? React with 👍 / 👎. |
||
| @update:width="sessionColWidth = $event" | ||
|
Comment on lines
572
to
574
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a user widens the sidebar on a large display and later opens the app in a narrower window, the persisted Useful? React with 👍 / 👎. |
||
| /> | ||
| <div v-if="sidebarCollapsed" class="sidebar-rail"> | ||
|
|
@@ -686,7 +695,7 @@ function openPr(url: string): void { | |
| :storage-key="PREVIEW_WIDTH_KEY" | ||
| :default-width="previewDefaultWidth" | ||
| :min="PREVIEW_MIN" | ||
| :max="previewMaxWidth" | ||
| :max="previewMax" | ||
| reverse | ||
| :aria-label="t('layout.resizePreviewAria')" | ||
| @update:width="previewWidth = $event" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
| // up pointer events (pointerdown/move/up with capture, no text-selection while | ||
| // dragging). Used by the sidebar session column drag handle. | ||
|
|
||
| import { onBeforeUnmount, ref, type Ref } from 'vue'; | ||
| import { onBeforeUnmount, ref, toValue, type MaybeRefOrGetter, type Ref } from 'vue'; | ||
| import { safeGetString, safeSetString } from '../lib/storage'; | ||
|
|
||
| export interface UseResizableOptions { | ||
|
|
@@ -14,8 +14,9 @@ export interface UseResizableOptions { | |
| defaultWidth: number; | ||
| /** Smallest allowed width (px). */ | ||
| min: number; | ||
| /** Largest allowed width (px). */ | ||
| max: number; | ||
| /** Largest allowed width (px). Accepts a ref/getter so a cap derived from the | ||
| * viewport keeps working as the window is resized after the handle mounts. */ | ||
| max: MaybeRefOrGetter<number>; | ||
| /** True when dragging right should shrink the controlled width. */ | ||
| reverse?: boolean; | ||
| } | ||
|
|
@@ -57,7 +58,7 @@ export function useResizable(options: UseResizableOptions): UseResizable { | |
|
|
||
| function clamp(value: number): number { | ||
| if (!Number.isFinite(value)) return defaultWidth; | ||
| return Math.min(max, Math.max(min, Math.round(value))); | ||
| return Math.min(toValue(max), Math.max(min, Math.round(value))); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| const width = ref<number>(clamp(readStored(storageKey) ?? defaultWidth)); | ||
|
|
@@ -107,7 +108,10 @@ export function useResizable(options: UseResizableOptions): UseResizable { | |
| event.preventDefault(); | ||
| dragging.value = true; | ||
| startX = event.clientX; | ||
| startWidth = width.value; | ||
| // The stored width can exceed the current cap (e.g. after the window narrows | ||
| // or a side panel opens). Clamp the drag start so the handle responds | ||
| // immediately instead of first covering an invisible delta. | ||
| startWidth = clamp(width.value); | ||
| activeEl = event.currentTarget as HTMLElement; | ||
| activePointerId = event.pointerId; | ||
| // Suppress text selection / show a resize cursor for the whole drag. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,21 +2,45 @@ | |
| // Layout: resizable session column. ResizeHandle owns the column width (with | ||
| // localStorage persistence); we mirror it here to drive the App grid. | ||
|
|
||
| import { computed, ref } from 'vue'; | ||
| import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'; | ||
| import { safeGetString, safeSetString, STORAGE_KEYS } from '../lib/storage'; | ||
| import { PREVIEW_MIN } from './useDetailPanel'; | ||
| import { clampPanelWidth, panelMaxWidth, useViewportWidth } from './useViewportWidth'; | ||
|
|
||
| const SIDEBAR_WIDTH_KEY = STORAGE_KEYS.sidebarWidth; | ||
| const SIDEBAR_COLLAPSED_KEY = STORAGE_KEYS.sidebarCollapsed; | ||
| const SIDEBAR_DEFAULT = 270; | ||
| const SIDEBAR_MIN = 170; | ||
| const SIDEBAR_MAX = 420; | ||
| const SIDEBAR_COLLAPSED_WIDTH = 36; | ||
| // Minimum width kept for the conversation pane. The sidebar is capped so the | ||
| // conversation keeps at least this much room, which also guarantees the sidebar | ||
| // resize handle and collapse button stay inside the viewport even when a width | ||
| // saved on a wider display is restored on a narrower one. | ||
| const CONVERSATION_MIN = 320; | ||
|
|
||
| export function useSidebarLayout() { | ||
| export interface UseSidebarLayoutOptions { | ||
| /** True while the right-side detail/preview panel is open, so the sidebar | ||
| * reserves room for it in addition to the conversation pane. */ | ||
| previewOpen?: MaybeRefOrGetter<boolean>; | ||
| } | ||
|
|
||
| export function useSidebarLayout(options: UseSidebarLayoutOptions = {}) { | ||
| const { viewportWidth } = useViewportWidth(); | ||
| const sessionColWidth = ref(SIDEBAR_DEFAULT); | ||
| const sidebarCollapsed = ref(false); | ||
|
|
||
| // Largest sidebar width that still leaves the conversation pane usable. When | ||
| // the right-side panel is open, also reserves its minimum width so the | ||
| // conversation column can never be squeezed to nothing. | ||
| const sidebarMax = computed(() => { | ||
| const reserve = CONVERSATION_MIN + (toValue(options.previewOpen) ? PREVIEW_MIN : 0); | ||
| return panelMaxWidth(viewportWidth.value, SIDEBAR_MIN, reserve); | ||
| }); | ||
|
|
||
| const sideWidth = computed(() => | ||
| sidebarCollapsed.value ? SIDEBAR_COLLAPSED_WIDTH : sessionColWidth.value, | ||
| sidebarCollapsed.value | ||
| ? SIDEBAR_COLLAPSED_WIDTH | ||
| : clampPanelWidth(sessionColWidth.value, SIDEBAR_MIN, sidebarMax.value), | ||
| ); | ||
|
Comment on lines
40
to
44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a saved/sidebar width is above the new viewport-derived cap, this only clamps Useful? React with 👍 / 👎. |
||
|
|
||
| function loadSidebarCollapsed(): void { | ||
|
|
@@ -44,7 +68,7 @@ export function useSidebarLayout() { | |
| SIDEBAR_WIDTH_KEY, | ||
| SIDEBAR_DEFAULT, | ||
| SIDEBAR_MIN, | ||
| SIDEBAR_MAX, | ||
| sidebarMax, | ||
| sessionColWidth, | ||
| sidebarCollapsed, | ||
| sideWidth, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // apps/kimi-web/src/composables/useViewportWidth.ts | ||
| // Shared reactive viewport width for the resizable layout panels. A single | ||
| // window resize listener backs every consumer, so panels can cap themselves to | ||
| // the current window size without each wiring up their own listener. | ||
|
|
||
| import { onBeforeUnmount, onMounted, ref } from 'vue'; | ||
|
|
||
| const viewportWidth = ref(typeof window === 'undefined' ? 0 : window.innerWidth); | ||
| let subscribers = 0; | ||
| let listening = false; | ||
|
|
||
| function update(): void { | ||
| viewportWidth.value = window.innerWidth; | ||
| } | ||
|
|
||
| function startListening(): void { | ||
| if (listening || typeof window === 'undefined') return; | ||
| window.addEventListener('resize', update); | ||
| listening = true; | ||
| update(); | ||
| } | ||
|
|
||
| function stopListening(): void { | ||
| if (!listening || typeof window === 'undefined') return; | ||
| window.removeEventListener('resize', update); | ||
| listening = false; | ||
| } | ||
|
|
||
| /** Largest a panel may grow while keeping `reserve` px free for the rest of the | ||
| * layout. Never drops below the panel's own `min`. */ | ||
| export function panelMaxWidth(available: number, min: number, reserve: number): number { | ||
| return Math.max(min, available - reserve); | ||
| } | ||
|
|
||
| /** Clamp a panel's chosen width into its allowed [min, max] range. */ | ||
| export function clampPanelWidth(width: number, min: number, max: number): number { | ||
| return Math.min(max, Math.max(min, width)); | ||
| } | ||
|
|
||
| export function useViewportWidth() { | ||
| onMounted(() => { | ||
| subscribers += 1; | ||
| startListening(); | ||
| }); | ||
| onBeforeUnmount(() => { | ||
| subscribers = Math.max(0, subscribers - 1); | ||
| if (subscribers === 0) stopListening(); | ||
| }); | ||
| return { viewportWidth }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On desktop, when the user runs bare
/btwto close an open side chat,handleCommandcallsclient.closeSideChat()directly and leavesdetailTarget === 'btw'.sidePanelVisiblethen becomes false becauseclient.sideChatVisibleis false, but this computed still stays true, souseSidebarLayoutkeeps subtractingPREVIEW_MINfrom the sidebar maximum after the right panel is gone. That leaves the session sidebar capped atviewport - 640until some other action clearsdetailTarget; base this reservation on the actually visible panel state or cleardetailTargeton the/btwclose path.Useful? React with 👍 / 👎.