Skip to content

Commit 67630e8

Browse files
authored
feat(ui): make the sidebar resizable (#102491)
* feat(ui): make the sidebar resizable * chore: defer release notes to release automation
1 parent 04b64ff commit 67630e8

9 files changed

Lines changed: 145 additions & 18 deletions

File tree

ui/src/app/app-host.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import "../components/exec-approval.ts";
1111
import "../components/gateway-url-confirmation.ts";
1212
import "../components/github-link-hovercard.ts";
1313
import "../components/login-gate.ts";
14+
import "../components/resizable-divider.ts";
1415
import "../components/terminal/terminal-panel.ts";
1516
import "../components/tooltip.ts";
1617
import "../components/update-banner.ts";
@@ -41,6 +42,7 @@ import { hasOperatorAdminAccess } from "./operator-access.ts";
4142
import type { ApplicationOverlaySnapshot } from "./overlays.ts";
4243
import { controlUiPublicAssetPath } from "./public-assets.ts";
4344
import { selectRenderedRouteMatch } from "./router-outlet.ts";
45+
import { NAV_WIDTH_DEFAULT, NAV_WIDTH_MAX, NAV_WIDTH_MIN } from "./settings.ts";
4446

4547
type ShellRouteState = {
4648
routeId?: RouteId;
@@ -372,6 +374,7 @@ class OpenClawShell extends LitElement {
372374
private context?: ApplicationContext<RouteId>;
373375

374376
@state() private navCollapsed = false;
377+
@state() private navWidth = NAV_WIDTH_DEFAULT;
375378
@state() private sidebarPinnedRoutes: readonly SidebarNavRoute[] = [];
376379
@state() private sidebarMoreExpanded = false;
377380
@state() private navDrawerOpen = false;
@@ -418,6 +421,7 @@ class OpenClawShell extends LitElement {
418421
this.startSubscriptions();
419422
this.addEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget);
420423
document.addEventListener("keydown", this.handleDocumentKeydown);
424+
window.addEventListener("resize", this.handleWindowResize);
421425
}
422426

423427
override updated() {
@@ -486,6 +490,7 @@ class OpenClawShell extends LitElement {
486490
override disconnectedCallback() {
487491
this.removeEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget);
488492
document.removeEventListener("keydown", this.handleDocumentKeydown);
493+
window.removeEventListener("resize", this.handleWindowResize);
489494
this.stopAgentsSubscription?.();
490495
this.stopAgentsSubscription = undefined;
491496
this.stopConfigSubscription?.();
@@ -576,6 +581,22 @@ class OpenClawShell extends LitElement {
576581
});
577582
}
578583

584+
private resizeNavigation(splitRatio: number) {
585+
const shell = this.querySelector<HTMLElement>(".shell");
586+
const context = this.context;
587+
if (!shell || !context) {
588+
return;
589+
}
590+
const navWidth = Math.round(
591+
Math.min(NAV_WIDTH_MAX, Math.max(NAV_WIDTH_MIN, splitRatio * shell.clientWidth)),
592+
);
593+
context.navigation.update({ navWidth });
594+
}
595+
596+
private readonly handleWindowResize = () => {
597+
this.requestUpdate();
598+
};
599+
579600
private readonly handleShellKeydown = (event: KeyboardEvent) => {
580601
if (event.defaultPrevented || event.key !== "Escape" || !this.navDrawerOpen) {
581602
return;
@@ -731,6 +752,7 @@ class OpenClawShell extends LitElement {
731752
snapshot: ApplicationRuntime["context"]["navigation"]["snapshot"],
732753
) => {
733754
this.navCollapsed = snapshot.navCollapsed;
755+
this.navWidth = snapshot.navWidth;
734756
this.sidebarPinnedRoutes = snapshot.sidebarPinnedRoutes;
735757
this.sidebarMoreExpanded = snapshot.sidebarMoreExpanded;
736758
};
@@ -751,6 +773,7 @@ class OpenClawShell extends LitElement {
751773
// Drawer navigation always opens expanded; the desktop collapse preference
752774
// stays persisted for when the viewport returns to the desktop layout.
753775
const navCollapsed = this.navCollapsed && !navDrawerOpen;
776+
const shellWidth = Math.max(globalThis.innerWidth || 0, NAV_WIDTH_MAX);
754777
return html`
755778
<openclaw-command-palette
756779
.onNavigate=${(routeId: RouteId) => this.navigate(routeId)}
@@ -766,6 +789,7 @@ class OpenClawShell extends LitElement {
766789
: ""} ${navDrawerOpen ? "shell--nav-drawer-open" : ""} ${this.onboarding
767790
? "shell--onboarding"
768791
: ""}"
792+
style=${`--shell-nav-expanded-width: ${this.navWidth}px`}
769793
@keydown=${this.handleShellKeydown}
770794
@theme-change=${this.handleThemeChange}
771795
>
@@ -817,6 +841,21 @@ class OpenClawShell extends LitElement {
817841
isRouteId(routeId) ? context.preload(routeId) : Promise.resolve()}
818842
></openclaw-app-sidebar>
819843
</div>
844+
${!navCollapsed && !this.onboarding
845+
? html`
846+
<resizable-divider
847+
class="sidebar-resizer"
848+
.label=${t("nav.resize")}
849+
.splitRatio=${this.navWidth / shellWidth}
850+
.minRatio=${NAV_WIDTH_MIN / shellWidth}
851+
.maxRatio=${NAV_WIDTH_MAX / shellWidth}
852+
aria-valuetext=${`${this.navWidth} pixels`}
853+
title=${t("nav.resize")}
854+
@resize=${(event: CustomEvent<{ splitRatio: number }>) =>
855+
this.resizeNavigation(event.detail.splitRatio)}
856+
></resizable-divider>
857+
`
858+
: nothing}
820859
<main
821860
class="content ${activeRoute === "chat" ? "content--chat" : ""} ${activeRoute ===
822861
"workboard"

ui/src/app/bootstrap.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ function createApplicationNavigationPreferences(
158158
let settings = initialSettings;
159159
let snapshot: ApplicationNavigationPreferencesSnapshot = {
160160
navCollapsed: settings.navCollapsed,
161+
navWidth: settings.navWidth,
161162
sidebarPinnedRoutes: settings.sidebarPinnedRoutes,
162163
sidebarMoreExpanded: settings.sidebarMoreExpanded,
163164
};
@@ -171,13 +172,15 @@ function createApplicationNavigationPreferences(
171172
const nextSnapshot = { ...snapshot, ...patch };
172173
if (
173174
nextSnapshot.navCollapsed === snapshot.navCollapsed &&
175+
nextSnapshot.navWidth === snapshot.navWidth &&
174176
nextSnapshot.sidebarPinnedRoutes === snapshot.sidebarPinnedRoutes &&
175177
nextSnapshot.sidebarMoreExpanded === snapshot.sidebarMoreExpanded
176178
) {
177179
return;
178180
}
179181
settings = patchSettings({
180182
navCollapsed: nextSnapshot.navCollapsed,
183+
navWidth: nextSnapshot.navWidth,
181184
sidebarPinnedRoutes: [...nextSnapshot.sidebarPinnedRoutes],
182185
sidebarMoreExpanded: nextSnapshot.sidebarMoreExpanded,
183186
});

ui/src/app/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type ApplicationTheme = {
3232

3333
export type ApplicationNavigationPreferencesSnapshot = {
3434
navCollapsed: boolean;
35+
navWidth: number;
3536
sidebarPinnedRoutes: readonly SidebarNavRoute[];
3637
sidebarMoreExpanded: boolean;
3738
};

ui/src/app/settings.node.test.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function makeSettings(gatewayUrl: string, overrides: Partial<UiSettings> = {}):
5858
chatShowToolCalls: true,
5959
splitRatio: 0.6,
6060
navCollapsed: false,
61-
navWidth: 220,
61+
navWidth: 258,
6262
sidebarPinnedRoutes: ["overview"],
6363
sidebarMoreExpanded: false,
6464
borderRadius: 50,
@@ -167,7 +167,7 @@ describe("loadSettings default gateway URL derivation", () => {
167167
chatAutoScroll: "near-bottom",
168168
splitRatio: 0.6,
169169
navCollapsed: false,
170-
navWidth: 220,
170+
navWidth: 258,
171171
sidebarPinnedRoutes: ["overview"],
172172
sidebarMoreExpanded: false,
173173
borderRadius: 50,
@@ -202,7 +202,7 @@ describe("loadSettings default gateway URL derivation", () => {
202202
chatAutoScroll: "near-bottom",
203203
splitRatio: 0.6,
204204
navCollapsed: false,
205-
navWidth: 220,
205+
navWidth: 258,
206206
sidebarPinnedRoutes: ["overview"],
207207
sidebarMoreExpanded: false,
208208
borderRadius: 50,
@@ -235,7 +235,7 @@ describe("loadSettings default gateway URL derivation", () => {
235235
chatAutoScroll: "near-bottom",
236236
splitRatio: 0.6,
237237
navCollapsed: false,
238-
navWidth: 220,
238+
navWidth: 258,
239239
sidebarPinnedRoutes: ["overview"],
240240
sidebarMoreExpanded: false,
241241
borderRadius: 50,
@@ -253,7 +253,7 @@ describe("loadSettings default gateway URL derivation", () => {
253253
chatAutoScroll: "near-bottom",
254254
splitRatio: 0.6,
255255
navCollapsed: false,
256-
navWidth: 220,
256+
navWidth: 258,
257257
sidebarPinnedRoutes: ["overview"],
258258
sidebarMoreExpanded: false,
259259
borderRadius: 50,
@@ -283,7 +283,7 @@ describe("loadSettings default gateway URL derivation", () => {
283283
chatShowToolCalls: true,
284284
splitRatio: 0.6,
285285
navCollapsed: false,
286-
navWidth: 220,
286+
navWidth: 258,
287287
sidebarPinnedRoutes: ["overview"],
288288
sidebarMoreExpanded: false,
289289
borderRadius: 50,
@@ -303,7 +303,7 @@ describe("loadSettings default gateway URL derivation", () => {
303303
chatAutoScroll: "near-bottom",
304304
splitRatio: 0.6,
305305
navCollapsed: false,
306-
navWidth: 220,
306+
navWidth: 258,
307307
sidebarPinnedRoutes: ["overview"],
308308
sidebarMoreExpanded: false,
309309
borderRadius: 50,
@@ -338,7 +338,7 @@ describe("loadSettings default gateway URL derivation", () => {
338338
chatAutoScroll: "near-bottom",
339339
splitRatio: 0.6,
340340
navCollapsed: false,
341-
navWidth: 220,
341+
navWidth: 258,
342342
sidebarPinnedRoutes: ["sessions", "cron"],
343343
sidebarMoreExpanded: true,
344344
borderRadius: 50,
@@ -347,6 +347,7 @@ describe("loadSettings default gateway URL derivation", () => {
347347

348348
expect(loadSettings().sidebarPinnedRoutes).toEqual(["sessions", "cron"]);
349349
expect(loadSettings().sidebarMoreExpanded).toBe(true);
350+
expect(loadSettings().navWidth).toBe(258);
350351

351352
// Corrupt the persisted list; load falls back to the default pinned set.
352353
const scopedKey = `openclaw.control.settings.v1:${gwUrl}`;
@@ -356,10 +357,12 @@ describe("loadSettings default gateway URL derivation", () => {
356357
>;
357358
persisted.sidebarPinnedRoutes = "sessions";
358359
persisted.sidebarMoreExpanded = "yes";
360+
persisted.navWidth = 220;
359361
localStorage.setItem(scopedKey, JSON.stringify(persisted));
360362

361363
expect(loadSettings().sidebarPinnedRoutes).toEqual(["overview"]);
362364
expect(loadSettings().sidebarMoreExpanded).toBe(false);
365+
expect(loadSettings().navWidth).toBe(258);
363366
});
364367

365368
it("normalizes persisted text scale to the nearest supported stop", () => {
@@ -475,7 +478,7 @@ describe("loadSettings default gateway URL derivation", () => {
475478
chatShowToolCalls: true,
476479
splitRatio: 0.6,
477480
navCollapsed: false,
478-
navWidth: 220,
481+
navWidth: 258,
479482
sidebarPinnedRoutes: ["overview"],
480483
sidebarMoreExpanded: false,
481484
borderRadius: 50,
@@ -491,7 +494,7 @@ describe("loadSettings default gateway URL derivation", () => {
491494
chatShowToolCalls: true,
492495
splitRatio: 0.6,
493496
navCollapsed: false,
494-
navWidth: 220,
497+
navWidth: 258,
495498
sidebarPinnedRoutes: ["overview"],
496499
sidebarMoreExpanded: false,
497500
borderRadius: 50,
@@ -592,7 +595,7 @@ describe("loadSettings default gateway URL derivation", () => {
592595
chatShowToolCalls: true,
593596
splitRatio: 0.6,
594597
navCollapsed: false,
595-
navWidth: 220,
598+
navWidth: 258,
596599
sidebarPinnedRoutes: ["overview"],
597600
sidebarMoreExpanded: false,
598601
borderRadius: 50,
@@ -623,7 +626,7 @@ describe("loadSettings default gateway URL derivation", () => {
623626
chatShowToolCalls: true,
624627
splitRatio: 0.6,
625628
navCollapsed: false,
626-
navWidth: 220,
629+
navWidth: 258,
627630
sidebarPinnedRoutes: ["overview"],
628631
sidebarMoreExpanded: false,
629632
borderRadius: 50,
@@ -668,7 +671,7 @@ describe("loadSettings default gateway URL derivation", () => {
668671
chatShowToolCalls: true,
669672
splitRatio: 0.6,
670673
navCollapsed: false,
671-
navWidth: 220,
674+
navWidth: 258,
672675
sidebarPinnedRoutes: ["overview"],
673676
sidebarMoreExpanded: false,
674677
borderRadius: 50,
@@ -712,7 +715,7 @@ describe("loadSettings default gateway URL derivation", () => {
712715
chatShowToolCalls: true,
713716
splitRatio: 0.6,
714717
navCollapsed: false,
715-
navWidth: 220,
718+
navWidth: 258,
716719
sidebarPinnedRoutes: ["overview"],
717720
sidebarMoreExpanded: false,
718721
borderRadius: 50,

ui/src/app/settings.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Control UI module implements storage behavior.
22
const SETTINGS_KEY_PREFIX = "openclaw.control.settings.v1:";
33
const LEGACY_SETTINGS_KEY = "openclaw.control.settings.v1";
4+
export const NAV_WIDTH_MIN = 240;
5+
export const NAV_WIDTH_MAX = 400;
6+
export const NAV_WIDTH_DEFAULT = 258;
47
const CURRENT_GATEWAY_SELECTION_KEY_PREFIX = "openclaw.control.currentGateway.v1:";
58
const LOCAL_USER_IDENTITY_KEY = "openclaw.control.user.v1";
69
const LEGACY_TOKEN_SESSION_KEY = "openclaw.control.token.v1";
@@ -532,7 +535,7 @@ export function loadSettings(): UiSettings {
532535
chatSendShortcut: "enter",
533536
splitRatio: 0.6,
534537
navCollapsed: false,
535-
navWidth: 220,
538+
navWidth: NAV_WIDTH_DEFAULT,
536539
sidebarPinnedRoutes: [...DEFAULT_SIDEBAR_PINNED_ROUTES],
537540
sidebarMoreExpanded: false,
538541
borderRadius: 50,
@@ -599,7 +602,9 @@ export function loadSettings(): UiSettings {
599602
navCollapsed:
600603
typeof parsed.navCollapsed === "boolean" ? parsed.navCollapsed : defaults.navCollapsed,
601604
navWidth:
602-
typeof parsed.navWidth === "number" && parsed.navWidth >= 200 && parsed.navWidth <= 400
605+
typeof parsed.navWidth === "number" &&
606+
parsed.navWidth >= NAV_WIDTH_MIN &&
607+
parsed.navWidth <= NAV_WIDTH_MAX
603608
? parsed.navWidth
604609
: defaults.navWidth,
605610
sidebarPinnedRoutes:

0 commit comments

Comments
 (0)