Skip to content

Commit 156f4c8

Browse files
authored
refactor(ui): fold sidebar More section into a popup menu (#105906)
The sidebar's expandable "MORE" section (uppercase header, chevron, persisted sidebarMoreExpanded pref, dedicated "Edit pinned items" row) collapses into a single More nav row that opens a transient popup menu with the unpinned routes, dynamic plugin tabs, and the pin editor entry. Menu rows are real anchors, so Cmd/middle-click keeps native link behavior; the More row carries the active highlight when the current route lives inside the menu. The sessions list gets the reclaimed vertical space, and one persisted setting is gone (stale keys are ignored on load). The nav rows and menus move to app-sidebar-nav-menus.ts to keep app-sidebar.ts under the TS LOC ratchet. Docs: docs/web/control-ui.md sidebar-navigation section updated. Fixes #105905
1 parent 0d90ca6 commit 156f4c8

15 files changed

Lines changed: 487 additions & 346 deletions

docs/web/control-ui.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ local-path sources, updates, and advanced plugin configuration.
168168

169169
## Sidebar navigation
170170

171-
The sidebar pins navigation above a scrollable session list. In multi-agent setups every agent appears as a collapsible top-level section; expanding an agent browses its sessions without navigating away from the open chat, and collapsed agents show an unread indicator. Within an agent the list splits into **Pinned**, one built-in section per connected channel (Telegram, Slack, WhatsApp, ...), a built-in **Work** section for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), custom groups (the session `category`), and **Chats** for the rest. Channel and Work sections classify rows automatically; assigning a session to a custom group always wins. Opening a session moves the selection highlight without reordering the rows. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Each session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Fork, Move to group (including New group and Remove from group), Archive, and Delete; touch layouts keep the direct pin and menu controls visible. Cmd/Ctrl-click toggles rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a session onto a custom group or **Chats** to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names and their order live in the gateway (`sessions.groups.*`), so they follow you across browsers, while the collapsed state stays in the browser profile. Group headers also have a menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Chats. The single **+** in the session-list header opens the New session page (see below). The sort control also has a Group by toggle: Grouped (default) or None for one flat list (Pinned stays separate); the choice is stored in the current browser profile. **Usage**, **Automations**, and **Plugins** are pinned by default; expand **More** to reach every other destination. Select **Edit pinned items** under More, or right-click the navigation area, to pin or unpin destinations and restore the defaults. The pinned set and More expansion state are stored in the current browser profile and survive reloads.
171+
The sidebar pins navigation above a scrollable session list. In multi-agent setups every agent appears as a collapsible top-level section; expanding an agent browses its sessions without navigating away from the open chat, and collapsed agents show an unread indicator. Within an agent the list splits into **Pinned**, one built-in section per connected channel (Telegram, Slack, WhatsApp, ...), a built-in **Work** section for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), custom groups (the session `category`), and **Chats** for the rest. Channel and Work sections classify rows automatically; assigning a session to a custom group always wins. Opening a session moves the selection highlight without reordering the rows. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Each session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Fork, Move to group (including New group and Remove from group), Archive, and Delete; touch layouts keep the direct pin and menu controls visible. Cmd/Ctrl-click toggles rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a session onto a custom group or **Chats** to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names and their order live in the gateway (`sessions.groups.*`), so they follow you across browsers, while the collapsed state stays in the browser profile. Group headers also have a menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Chats. The single **+** in the session-list header opens the New session page (see below). The sort control also has a Group by toggle: Grouped (default) or None for one flat list (Pinned stays separate); the choice is stored in the current browser profile. **Usage**, **Automations**, and **Plugins** are pinned by default; the **More** row opens a menu with every other destination, including plugin-provided tabs. Select **Edit pinned items** in that menu, or right-click the navigation area, to pin or unpin destinations and restore the defaults. The pinned set is stored in the current browser profile and survives reloads.
172172

173173
## New session page
174174

scripts/ts-max-loc-baseline.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,11 +1170,11 @@
11701170
"tsdown.config.ts": 800,
11711171
"ui/src/api/gateway.ts": 1211,
11721172
"ui/src/api/types.ts": 865,
1173-
"ui/src/app/app-host.ts": 1204,
1173+
"ui/src/app/app-host.ts": 1199,
11741174
"ui/src/app/custom-theme.ts": 648,
11751175
"ui/src/app/overlays.ts": 714,
1176-
"ui/src/app/settings.ts": 711,
1177-
"ui/src/components/app-sidebar.ts": 2999,
1176+
"ui/src/app/settings.ts": 704,
1177+
"ui/src/components/app-sidebar.ts": 2949,
11781178
"ui/src/components/browser/browser-panel.ts": 1412,
11791179
"ui/src/components/command-palette.ts": 687,
11801180
"ui/src/components/config-form.node.ts": 1169,

ui/src/app/app-host.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,6 @@ class OpenClawShell extends OpenClawLightDomElement {
11121112
.canPairDevice=${gatewaySnapshot.connected &&
11131113
hasOperatorAdminAccess(gatewaySnapshot.hello?.auth ?? null)}
11141114
.sidebarPinnedRoutes=${navigationSnapshot.sidebarPinnedRoutes}
1115-
.sidebarMoreExpanded=${navigationSnapshot.sidebarMoreExpanded}
11161115
.themeMode=${context.theme.mode}
11171116
.lobsterPetVisits=${uiSettings.lobsterPetVisits !== false}
11181117
.lobsterPetSounds=${uiSettings.lobsterPetSounds === true}
@@ -1131,10 +1130,6 @@ class OpenClawShell extends OpenClawLightDomElement {
11311130
});
11321131
}}
11331132
.draftSessionAgentId=${this.draftSessionAgentId()}
1134-
.onToggleMore=${() =>
1135-
context.navigation.update({
1136-
sidebarMoreExpanded: !context.navigation.snapshot.sidebarMoreExpanded,
1137-
})}
11381133
.onUpdatePinnedRoutes=${(routes: SidebarNavRoute[]) =>
11391134
context.navigation.update({ sidebarPinnedRoutes: routes })}
11401135
.onPairMobile=${() => void context.overlays.openDevicePairSetup()}

ui/src/app/bootstrap.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ function createApplicationNavigationPreferences(
164164
navCollapsed: settings.navCollapsed,
165165
navWidth: settings.navWidth,
166166
sidebarPinnedRoutes: settings.sidebarPinnedRoutes,
167-
sidebarMoreExpanded: settings.sidebarMoreExpanded,
168167
};
169168
const listeners = new Set<(next: ApplicationNavigationPreferencesSnapshot) => void>();
170169

@@ -177,16 +176,14 @@ function createApplicationNavigationPreferences(
177176
if (
178177
nextSnapshot.navCollapsed === snapshot.navCollapsed &&
179178
nextSnapshot.navWidth === snapshot.navWidth &&
180-
nextSnapshot.sidebarPinnedRoutes === snapshot.sidebarPinnedRoutes &&
181-
nextSnapshot.sidebarMoreExpanded === snapshot.sidebarMoreExpanded
179+
nextSnapshot.sidebarPinnedRoutes === snapshot.sidebarPinnedRoutes
182180
) {
183181
return;
184182
}
185183
settings = patchSettings({
186184
navCollapsed: nextSnapshot.navCollapsed,
187185
navWidth: nextSnapshot.navWidth,
188186
sidebarPinnedRoutes: [...nextSnapshot.sidebarPinnedRoutes],
189-
sidebarMoreExpanded: nextSnapshot.sidebarMoreExpanded,
190187
});
191188
snapshot = nextSnapshot;
192189
for (const listener of listeners) {

ui/src/app/context.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export type ApplicationNavigationPreferencesSnapshot = {
3434
navCollapsed: boolean;
3535
navWidth: number;
3636
sidebarPinnedRoutes: readonly SidebarNavRoute[];
37-
sidebarMoreExpanded: boolean;
3837
};
3938

4039
export type ApplicationNavigationPreferences = {

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ function makeSettings(gatewayUrl: string, overrides: Partial<UiSettings> = {}):
6262
navCollapsed: false,
6363
navWidth: 258,
6464
sidebarPinnedRoutes: [],
65-
sidebarMoreExpanded: false,
6665
...overrides,
6766
};
6867
}
@@ -262,7 +261,6 @@ describe("loadSettings default gateway URL derivation", () => {
262261
navCollapsed: false,
263262
navWidth: 258,
264263
sidebarPinnedRoutes: [],
265-
sidebarMoreExpanded: false,
266264
textScale: 100,
267265
});
268266

@@ -294,7 +292,6 @@ describe("loadSettings default gateway URL derivation", () => {
294292
navCollapsed: false,
295293
navWidth: 258,
296294
sidebarPinnedRoutes: [],
297-
sidebarMoreExpanded: false,
298295
});
299296

300297
saveSettings({
@@ -311,7 +308,6 @@ describe("loadSettings default gateway URL derivation", () => {
311308
navCollapsed: false,
312309
navWidth: 258,
313310
sidebarPinnedRoutes: [],
314-
sidebarMoreExpanded: false,
315311
});
316312

317313
const settings = loadSettings();
@@ -340,7 +336,6 @@ describe("loadSettings default gateway URL derivation", () => {
340336
navCollapsed: false,
341337
navWidth: 258,
342338
sidebarPinnedRoutes: [],
343-
sidebarMoreExpanded: false,
344339
});
345340
const settings = loadSettings();
346341
expect(settings.gatewayUrl).toBe(gwUrl);
@@ -359,7 +354,6 @@ describe("loadSettings default gateway URL derivation", () => {
359354
navCollapsed: false,
360355
navWidth: 258,
361356
sidebarPinnedRoutes: [],
362-
sidebarMoreExpanded: false,
363357
textScale: 100,
364358
sessionsByGateway: {
365359
[gwUrl]: {
@@ -393,12 +387,10 @@ describe("loadSettings default gateway URL derivation", () => {
393387
navCollapsed: false,
394388
navWidth: 258,
395389
sidebarPinnedRoutes: ["sessions", "cron"],
396-
sidebarMoreExpanded: true,
397390
textScale: 100,
398391
});
399392

400393
expect(loadSettings().sidebarPinnedRoutes).toEqual(["sessions", "cron"]);
401-
expect(loadSettings().sidebarMoreExpanded).toBe(true);
402394
expect(loadSettings().navWidth).toBe(258);
403395

404396
// Corrupt the persisted list; load falls back to the default pinned set.
@@ -408,12 +400,10 @@ describe("loadSettings default gateway URL derivation", () => {
408400
unknown
409401
>;
410402
persisted.sidebarPinnedRoutes = "sessions";
411-
persisted.sidebarMoreExpanded = "yes";
412403
persisted.navWidth = 220;
413404
localStorage.setItem(scopedKey, JSON.stringify(persisted));
414405

415406
expect(loadSettings().sidebarPinnedRoutes).toEqual(["usage", "cron", "plugins"]);
416-
expect(loadSettings().sidebarMoreExpanded).toBe(false);
417407
expect(loadSettings().navWidth).toBe(258);
418408
});
419409

@@ -532,7 +522,6 @@ describe("loadSettings default gateway URL derivation", () => {
532522
navCollapsed: false,
533523
navWidth: 258,
534524
sidebarPinnedRoutes: [],
535-
sidebarMoreExpanded: false,
536525
});
537526
saveSettings({
538527
gatewayUrl: gwUrl,
@@ -547,7 +536,6 @@ describe("loadSettings default gateway URL derivation", () => {
547536
navCollapsed: false,
548537
navWidth: 258,
549538
sidebarPinnedRoutes: [],
550-
sidebarMoreExpanded: false,
551539
});
552540

553541
expect(loadSettings().token).toBe("");
@@ -575,7 +563,6 @@ describe("loadSettings default gateway URL derivation", () => {
575563
navCollapsed: false,
576564
navWidth: 320,
577565
sidebarPinnedRoutes: [],
578-
sidebarMoreExpanded: false,
579566
});
580567

581568
const scopedKey = `openclaw.control.settings.v1:${gwUrl}`;
@@ -646,7 +633,6 @@ describe("loadSettings default gateway URL derivation", () => {
646633
navCollapsed: false,
647634
navWidth: 258,
648635
sidebarPinnedRoutes: [],
649-
sidebarMoreExpanded: false,
650636
customTheme,
651637
});
652638

@@ -676,7 +662,6 @@ describe("loadSettings default gateway URL derivation", () => {
676662
navCollapsed: false,
677663
navWidth: 258,
678664
sidebarPinnedRoutes: [],
679-
sidebarMoreExpanded: false,
680665
customTheme: {
681666
sourceUrl: "https://tweakcn.com/themes/broken",
682667
themeId: "broken",
@@ -720,7 +705,6 @@ describe("loadSettings default gateway URL derivation", () => {
720705
navCollapsed: false,
721706
navWidth: 258,
722707
sidebarPinnedRoutes: [],
723-
sidebarMoreExpanded: false,
724708
});
725709

726710
const settings = loadSettings();
@@ -763,7 +747,6 @@ describe("loadSettings default gateway URL derivation", () => {
763747
navCollapsed: false,
764748
navWidth: 258,
765749
sidebarPinnedRoutes: [],
766-
sidebarMoreExpanded: false,
767750
});
768751

769752
const persisted = JSON.parse(localStorage.getItem(scopedKey) ?? "{}");

ui/src/app/settings.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ export type UiSettings = {
109109
chatWorkspaceDock?: ChatWorkspaceDock; // Session workspace rail dock edge (default "right")
110110
navCollapsed: boolean; // Collapsible sidebar state
111111
navWidth: number; // Sidebar width when expanded (240–400px)
112-
sidebarPinnedRoutes: SidebarNavRoute[]; // Nav routes shown above the "More" section
113-
sidebarMoreExpanded: boolean; // Whether the sidebar "More" section is expanded
112+
sidebarPinnedRoutes: SidebarNavRoute[]; // Nav routes shown above the "More" menu row
114113
textScale?: TextScaleStop; // Browser-local text scale percentage
115114
customTheme?: ImportedCustomTheme;
116115
locale?: string;
@@ -520,7 +519,6 @@ export function loadSettings(): UiSettings {
520519
navCollapsed: false,
521520
navWidth: NAV_WIDTH_DEFAULT,
522521
sidebarPinnedRoutes: [...DEFAULT_SIDEBAR_PINNED_ROUTES],
523-
sidebarMoreExpanded: false,
524522
textScale: 100,
525523
};
526524

@@ -586,10 +584,6 @@ export function loadSettings(): UiSettings {
586584
: defaults.navWidth,
587585
sidebarPinnedRoutes:
588586
normalizeSidebarPinnedRoutes(parsed.sidebarPinnedRoutes) ?? defaults.sidebarPinnedRoutes,
589-
sidebarMoreExpanded:
590-
typeof parsed.sidebarMoreExpanded === "boolean"
591-
? parsed.sidebarMoreExpanded
592-
: defaults.sidebarMoreExpanded,
593587
textScale: normalizeTextScale(parsed.textScale, defaults.textScale),
594588
customTheme: customTheme ?? undefined,
595589
locale: isSupportedLocale(parsed.locale) ? parsed.locale : undefined,
@@ -685,7 +679,6 @@ function persistSettings(next: UiSettings, options: { selectGateway?: boolean }
685679
navCollapsed: next.navCollapsed,
686680
navWidth: next.navWidth,
687681
sidebarPinnedRoutes: next.sidebarPinnedRoutes,
688-
sidebarMoreExpanded: next.sidebarMoreExpanded,
689682
textScale: normalizeTextScale(next.textScale),
690683
...(next.customTheme ? { customTheme: next.customTheme } : {}),
691684
sessionsByGateway,

0 commit comments

Comments
 (0)