Skip to content

Commit 170ae44

Browse files
authored
style(web): polish the session sidebar (#1519)
* feat(web): use sidebar fold/unfold icons for sidebar toggle * feat(web): move settings entry to a sidebar footer row * feat(web): fully collapse sidebar with animated width transition * feat(web): redesign sidebar colors, spacing and macos desktop chrome * feat(desktop): center traffic lights on the 48px header row * fix(web): restore webkit thin scrollbars and unify sidebar icon sizes * feat(web): add Kbd keycap component and justify sidebar search shortcut * style(web): rework sidebar palette and pin a resident sidebar toggle * fix(desktop): sync window appearance with web UI theme so dimmed traffic lights stay visible * feat(web): adopt Kimi design icons in the sidebar via a local icon collection * style(web): mute workspace group title color in the sidebar * style(web): refine sidebar typography, unify shortcut keycaps, float workspace row actions * style(web): cap sidebar draggable width at 480px * style(web): derive sidebar row height from type and padding, float the kebab * chore: add changeset for sidebar UI polish * fix(nix): update pnpmDeps hash * style(web): put the sidebar collapse button inside the header on non-mac * fix(nix): update pnpmDeps hash
1 parent 1bf2c9a commit 170ae44

28 files changed

Lines changed: 758 additions & 413 deletions

.changeset/polish-sidebar-ui.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
web: Polish the session sidebar layout, colors, icons, and typography.

apps/kimi-desktop/src/main/index.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
import { dirname, join } from 'node:path';
33

4-
import { app, BrowserWindow, Menu, shell } from 'electron';
4+
import { app, BrowserWindow, Menu, nativeTheme, shell } from 'electron';
55
import type { MenuItemConstructorOptions } from 'electron';
66

77
import { ensureServer, kimiHome, serverLogPath } from './ensure-server';
@@ -152,8 +152,13 @@ function createWindow(): void {
152152
title: 'Kimi Code Desktop',
153153
// macOS: hide the native title bar and float the traffic lights over the
154154
// content; the web UI reserves a draggable strip at the top to clear them.
155+
// 'hidden' (not 'hiddenInset') so trafficLightPosition can pin the lights
156+
// to the vertical center of the web UI's 48px header row (y 18 + 12px
157+
// button height / 2 = 24 = the header's midline — same line as the
158+
// sidebar-expand button and the conversation title).
155159
// 'default' on other platforms (they keep their native title bar).
156-
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
160+
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
161+
trafficLightPosition: { x: 16, y: 18 },
157162
webPreferences: {
158163
contextIsolation: true,
159164
nodeIntegration: false,
@@ -165,6 +170,63 @@ function createWindow(): void {
165170
win.webContents.on('page-title-updated', (event) => {
166171
event.preventDefault();
167172
});
173+
// macOS traffic lights.
174+
//
175+
// 1) Visibility across transitions: with titleBarStyle 'hidden' + a custom
176+
// trafficLightPosition, the buttons can vanish (or lose their custom
177+
// position) after a full-screen round-trip or on re-focus. Re-assert both
178+
// on those transitions (observed on Electron 33; belt-and-braces).
179+
//
180+
// 2) Blur is NOT such a case: unfocused traffic lights are merely DIMMED by
181+
// AppKit, and the dimmed color follows the WINDOW appearance, not the
182+
// page (electron#27295) — with the OS in dark mode but the web UI on a
183+
// light theme, the light-gray dimmed dots become invisible against the
184+
// light sidebar. That is fixed by the theme sync below, which keeps the
185+
// window appearance aligned with the web UI's <html data-color-scheme>.
186+
if (process.platform === 'darwin') {
187+
const showTrafficLights = (): void => {
188+
if (win.isDestroyed()) return;
189+
win.setWindowButtonPosition({ x: 16, y: 18 });
190+
win.setWindowButtonVisibility(true);
191+
};
192+
win.on('enter-full-screen', showTrafficLights);
193+
win.on('leave-full-screen', showTrafficLights);
194+
win.on('focus', showTrafficLights);
195+
196+
// Theme sync: no preload/IPC channel exists, so inject a tiny observer
197+
// that reports <html data-color-scheme> ('light' | 'dark' | 'system')
198+
// through a tagged console message, and mirror it into
199+
// nativeTheme.themeSource (same three states). The startup/error screens
200+
// (data: URLs) have no such attribute and harmlessly report 'system'.
201+
const THEME_TAG = '__kimi_desktop_theme__:';
202+
win.webContents.on('console-message', (_event, _level, message) => {
203+
if (!message.startsWith(THEME_TAG)) return;
204+
const scheme = message.slice(THEME_TAG.length);
205+
if (scheme === 'light' || scheme === 'dark' || scheme === 'system') {
206+
nativeTheme.themeSource = scheme;
207+
}
208+
});
209+
win.webContents.on('did-finish-load', () => {
210+
win.webContents
211+
.executeJavaScript(
212+
`(() => {
213+
const report = () => {
214+
const v = document.documentElement.dataset.colorScheme;
215+
console.info(${JSON.stringify(THEME_TAG)} + (v === 'light' || v === 'dark' ? v : 'system'));
216+
};
217+
new MutationObserver(report).observe(document.documentElement, {
218+
attributes: true,
219+
attributeFilter: ['data-color-scheme'],
220+
});
221+
report();
222+
})();`,
223+
)
224+
.catch(() => {
225+
// Navigation can tear the page down mid-injection; theme sync is
226+
// cosmetic, so ignore.
227+
});
228+
});
229+
}
168230
win.on('close', () => {
169231
saveBounds(win);
170232
});

apps/kimi-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
},
2929
"devDependencies": {
3030
"@iconify-json/ri": "^1.2.10",
31+
"@iconify-json/tabler": "^1.2.35",
3132
"@vitejs/plugin-vue": "^5.2.4",
3233
"typescript": "6.0.2",
3334
"unplugin-icons": "^23.0.0",

apps/kimi-web/src/App.vue

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import { stripSkillPrefix } from './lib/slashCommands';
4343
import Button from './components/ui/Button.vue';
4444
import IconButton from './components/ui/IconButton.vue';
4545
import Icon from './components/ui/Icon.vue';
46+
import InternalBuildBanner from './components/InternalBuildBanner.vue';
47+
import { isMacosDesktop } from './lib/desktopFlag';
4648
4749
// Hydrate the server-transport credential (fragment token or sessionStorage)
4850
// BEFORE the client connects, so the first REST/WS calls already carry it.
@@ -213,6 +215,7 @@ const {
213215
sidebarMax,
214216
sessionColWidth,
215217
sidebarCollapsed,
218+
sidebarDragging,
216219
sideWidth,
217220
loadSidebarCollapsed,
218221
toggleSidebarCollapse,
@@ -645,13 +648,18 @@ function openPr(url: string): void {
645648
<div
646649
v-else
647650
class="app"
648-
:class="{ mobile: isMobile, 'sidebar-collapsed': sidebarCollapsed && !isMobile }"
649-
:style="{ '--side-w': sideWidth + 'px', '--preview-w': previewPanelWidth + 'px' }"
651+
:class="{
652+
mobile: isMobile,
653+
'sidebar-collapsed': sidebarCollapsed && !isMobile,
654+
'macos-desktop': isMacosDesktop,
655+
}"
656+
:style="{ '--preview-w': previewPanelWidth + 'px' }"
650657
>
651658
<!-- Desktop navigation: workspace rail + resizable session column. -->
652659
<template v-if="!isMobile">
653660
<Sidebar
654-
v-show="!sidebarCollapsed"
661+
:collapsed="sidebarCollapsed"
662+
:dragging="sidebarDragging"
655663
:col-width="sideWidth"
656664
:active-workspace="client.visibleWorkspace.value"
657665
:active-workspace-id="client.activeWorkspaceId.value"
@@ -681,21 +689,14 @@ function openPr(url: string): void {
681689
/>
682690
<ResizeHandle
683691
v-show="!sidebarCollapsed"
692+
class="side-handle"
684693
:storage-key="SIDEBAR_WIDTH_KEY"
685694
:default-width="SIDEBAR_DEFAULT"
686695
:min="SIDEBAR_MIN"
687696
:max="sidebarMax"
688697
@update:width="sessionColWidth = $event"
698+
@update:dragging="sidebarDragging = $event"
689699
/>
690-
<div v-if="sidebarCollapsed" class="sidebar-rail">
691-
<IconButton
692-
size="sm"
693-
:label="t('sidebar.expandSidebar')"
694-
@click="toggleSidebarCollapse"
695-
>
696-
<Icon name="panel-expand" size="sm" />
697-
</IconButton>
698-
</div>
699700
</template>
700701

701702
<!-- Mobile navigation: slim top bar (switcher + settings sheets). -->
@@ -792,8 +793,29 @@ function openPr(url: string): void {
792793
@edit-message="handleEditMessage"
793794
/>
794795

796+
<!-- Sidebar toggle — floating only when the in-header control can't serve:
797+
on macOS desktop it's RESIDENT (always rendered beside the traffic
798+
lights, the sidebar slides underneath and only the glyph swaps, so it
799+
never moves or flashes); on Windows/web the collapse button lives
800+
inside the sidebar header, so this floating button only appears while
801+
COLLAPSED (to re-expand the sidebar). It must come AFTER
802+
ConversationPane in the DOM: Electron computes the window-drag region
803+
in tree order (drag rects union, no-drag rects subtract), so a no-drag
804+
element placed before the ChatHeader drag region would have its hole
805+
painted back over — making the button an inert drag area. -->
806+
<IconButton
807+
v-if="!isMobile && (isMacosDesktop || sidebarCollapsed)"
808+
class="sidebar-toggle-btn"
809+
size="sm"
810+
:label="sidebarCollapsed ? t('sidebar.expandSidebar') : t('sidebar.collapseSidebar')"
811+
@click="toggleSidebarCollapse"
812+
>
813+
<Icon :name="sidebarCollapsed ? 'panel-expand' : 'panel-collapse'" />
814+
</IconButton>
815+
795816
<ResizeHandle
796817
v-if="sidePanelVisible && !isMobile"
818+
class="preview-handle"
797819
:storage-key="PREVIEW_WIDTH_KEY"
798820
:default-width="previewDefaultWidth"
799821
:min="PREVIEW_MIN"
@@ -875,6 +897,11 @@ function openPr(url: string): void {
875897
/>
876898
</aside>
877899

900+
<!-- Internal-build tag — pinned to the app's bottom-right corner, above
901+
whatever pane happens to be there. Purely informational: pointer
902+
events pass through so it never blocks clicks. -->
903+
<InternalBuildBanner class="internal-build-fab" />
904+
878905
<!-- Model Picker overlay -->
879906
<ModelPicker
880907
v-if="showModelPicker"
@@ -1101,18 +1128,20 @@ function openPr(url: string): void {
11011128
color: var(--dim);
11021129
}
11031130
.app {
1104-
--side-w: 248px;
11051131
--preview-w: 460px;
11061132
flex: 1;
11071133
min-height: 0;
1134+
position: relative;
11081135
display: grid;
1109-
/* sidebar (rail + resizable session column) | 0-width handle | conversation.
1110-
The 4px ResizeHandle overflows its zero-width track via negative margins so
1111-
the whole strip is grabbable without consuming layout space. */
1112-
/* The right-panel track is PERMANENT (auto = follows the aside's width, 0
1113-
when closed) — opening animates the aside's width, so the conversation
1114-
column is squeezed over smoothly instead of snapping to a new template. */
1115-
grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto;
1136+
/* sidebar | 0-width handle | conversation | 0-width handle | right panel.
1137+
The 4px ResizeHandles overflow their zero-width tracks via negative margins
1138+
so the whole strip is grabbable without consuming layout space. */
1139+
/* Both side tracks are PERMANENT (auto = follows the aside's width, 0 when
1140+
closed/collapsed) — opening or collapsing animates the aside's width, so
1141+
the conversation column is squeezed over smoothly instead of snapping to a
1142+
new template. Every column is pinned explicitly (grid-column 1–5) so a
1143+
display:none handle can't shift auto-placement. */
1144+
grid-template-columns: auto 0 minmax(0, 1fr) 0 auto;
11161145
background: var(--bg);
11171146
color: var(--color-text);
11181147
overflow: hidden;
@@ -1126,20 +1155,50 @@ function openPr(url: string): void {
11261155
min-width: 0;
11271156
}
11281157
1129-
/* Collapsed sidebar rail: keeps a slim, dedicated grid track so the expand
1130-
button never overlaps the conversation header or squeezes the main pane. */
1131-
.sidebar-rail {
1132-
grid-column: 1;
1133-
display: flex;
1134-
justify-content: center;
1135-
padding-top: 8px;
1136-
background: var(--panel);
1137-
border-right: 1px solid var(--line);
1158+
/* Pin every desktop grid child to its track so auto-placement can never
1159+
reshuffle columns when a handle is display:none (v-show/v-if). */
1160+
.app > .side { grid-column: 1; }
1161+
.side-handle { grid-column: 2; }
1162+
.app:not(.mobile) > .con { grid-column: 3; }
1163+
.preview-handle { grid-column: 4; }
1164+
1165+
/* Sidebar toggle — floating button pinned to the top-left corner. On macOS
1166+
desktop it is resident (rendered in both states beside the traffic lights);
1167+
on Windows/web it only appears while the sidebar is collapsed (the collapse
1168+
button lives inside the sidebar header). While collapsed the conversation
1169+
header pads left so its content clears the button (global block below). */
1170+
.sidebar-toggle-btn {
1171+
position: absolute;
1172+
/* Vertically centered in the 48px conversation header. */
1173+
top: 11px;
1174+
left: 16px;
1175+
z-index: var(--z-sticky);
1176+
/* Fade in on appearance (Windows/web: only rendered while collapsed, so
1177+
this plays as the sidebar finishes sliding away). macOS disables it. */
1178+
animation: sidebar-toggle-btn-in 0.18s var(--ease-out) 0.12s backwards;
1179+
/* Floats over the macOS-desktop window-drag header; keep it clickable. */
1180+
-webkit-app-region: no-drag;
11381181
}
1139-
/* The collapsed rail occupies track 1; keep the main pane pinned to the
1140-
conversation track even though the sidebar/handle are display:none. */
1141-
.app.sidebar-collapsed > .con {
1142-
grid-column: 3;
1182+
/* macOS desktop (hidden title bar): resident beside the floating traffic
1183+
lights (green light's right edge ≈ 68px; 72 keeps a gap that matches the
1184+
lights' own 8px rhythm); no entrance animation since it never appears. */
1185+
.app.macos-desktop .sidebar-toggle-btn {
1186+
left: 72px;
1187+
animation: none;
1188+
}
1189+
@keyframes sidebar-toggle-btn-in {
1190+
from { opacity: 0; }
1191+
}
1192+
1193+
/* Internal-build tag pinned to the app's bottom-right corner (desktop app
1194+
only — the component renders nothing elsewhere). Informational: never
1195+
intercepts pointer input. */
1196+
.internal-build-fab {
1197+
position: absolute;
1198+
right: var(--space-3);
1199+
bottom: var(--space-3);
1200+
z-index: var(--z-sticky);
1201+
pointer-events: none;
11431202
}
11441203
11451204
/* Mobile single-column shell: slim top bar (auto) over the full-width
@@ -1208,4 +1267,19 @@ function openPr(url: string): void {
12081267
one continuous line across the layout. */
12091268
--panel-head-h: 48px;
12101269
}
1270+
1271+
/* Sidebar collapsed (desktop): the conversation header pads left so its
1272+
content clears the floating sidebar toggle (.sidebar-toggle-btn) — and the
1273+
macOS traffic lights on desktop builds. Animated in step with the sidebar
1274+
width transition. Cross-component rule (ChatHeader renders the header), so
1275+
it lives in this global block. */
1276+
.app:not(.mobile) .chat-header {
1277+
transition: padding-left 0.28s cubic-bezier(0.4, 0, 0.2, 1);
1278+
}
1279+
.app.sidebar-collapsed .chat-header {
1280+
padding-left: 52px;
1281+
}
1282+
.app.sidebar-collapsed.macos-desktop .chat-header {
1283+
padding-left: 108px;
1284+
}
12111285
</style>

apps/kimi-web/src/components/InternalBuildBanner.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { isDesktop } from '../lib/desktopFlag';
55
66
const { t } = useI18n();
77
8-
// True only inside the Kimi Desktop app (see desktopFlag.ts). Renders an inline
9-
// tag meant to sit next to the "Kimi Code" brand in the sidebar header.
8+
// True only inside the Kimi Desktop app (see desktopFlag.ts). Renders a small
9+
// tag pinned to the app's bottom-right corner (positioned by App.vue).
1010
const show = isDesktop;
1111
</script>
1212

0 commit comments

Comments
 (0)