diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index de16be6d2dcfb..656ceb2f6b102 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1940,6 +1940,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve ); }; const sortableIds = sidebarListItems.map(sidebarListItemId); + const sectionDividersEnabled = useClientSettings((s) => s.sidebarSectionDividersEnabled); // Keep non-drag orders constant to preserve sorting strategy and row identity during streaming. const { settledOrder: draggedSettledOrder, activeOrder: draggedActiveOrder } = dragState === null @@ -3355,7 +3356,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve key="pinned-header" marker="pinned-header" label="Pinned" - visible={from !== null} + visible={sectionDividersEnabled || from !== null} + persistent={sectionDividersEnabled} isDropTarget={dragTargetSection === "pinned"} />, ); @@ -3366,7 +3368,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve key="pinned-divider" marker="pinned-divider" label="Active" - visible={from !== null} + visible={sectionDividersEnabled || from !== null} + persistent={sectionDividersEnabled} isDropTarget={dragTargetSection === "active"} />, ); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index c4e3d27a07fa7..0dc56709d47eb 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -465,6 +465,10 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.sidebarWorkingShelfEnabled !== DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled ? ["Working section"] : []), + ...(settings.sidebarSectionDividersEnabled !== + DEFAULT_UNIFIED_SETTINGS.sidebarSectionDividersEnabled + ? ["Thread section dividers"] + : []), ...(settings.sidebarAutoSettleAfterDays !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ? ["Auto-settle inactive threads"] : []), @@ -631,6 +635,7 @@ export function useSettingsRestore(onRestored?: () => void) { sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, sidebarProjectSortOrder: DEFAULT_UNIFIED_SETTINGS.sidebarProjectSortOrder, sidebarWorkingShelfEnabled: DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled, + sidebarSectionDividersEnabled: DEFAULT_UNIFIED_SETTINGS.sidebarSectionDividersEnabled, sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, sidebarAutoSettleKeepPullRequests: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleKeepPullRequests, sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge, @@ -886,6 +891,33 @@ export function GeneralSettingsPanel() { /> } /> + + updateSettings({ + sidebarSectionDividersEnabled: + DEFAULT_UNIFIED_SETTINGS.sidebarSectionDividersEnabled, + }) + } + /> + ) : null + } + control={ + + updateSettings({ sidebarSectionDividersEnabled: Boolean(checked) }) + } + aria-label="Always show thread section dividers" + /> + } + /> diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 1d5c13e72a061..9d18c50aba42a 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -433,6 +433,12 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/general", searchTerms: ["hide fold running monitoring threads inbox sidebar shelf"], }, + { + id: "sidebar-section-dividers", + title: "Always show thread section dividers", + to: "/settings/general", + searchTerms: ["sidebar pinned active labels lines separators dragging"], + }, { id: "boring-sidebar", title: "Boring sidebar (group by project)", diff --git a/apps/web/src/components/sidebar/SidebarListMarkers.tsx b/apps/web/src/components/sidebar/SidebarListMarkers.tsx index 104bda5582b4d..ae8465cb8f9db 100644 --- a/apps/web/src/components/sidebar/SidebarListMarkers.tsx +++ b/apps/web/src/components/sidebar/SidebarListMarkers.tsx @@ -76,16 +76,20 @@ export function SidebarDragBoundary(props: { marker: "pinned-header" | "pinned-divider"; label: string; visible: boolean; + persistent: boolean; isDropTarget: boolean; }) { return ( {props.visible ? ( -
+
{ }); }); +describe("ClientSettings sidebar section dividers", () => { + it("keeps existing clients opted out and preserves explicit choices", () => { + expect(decodeClientSettings({}).sidebarSectionDividersEnabled).toBe(false); + expect(decodeClientSettingsPatch({})).not.toHaveProperty("sidebarSectionDividersEnabled"); + for (const enabled of [true, false]) { + const patch = { sidebarSectionDividersEnabled: enabled }; + expect(decodeClientSettingsPatch(patch)).toEqual(patch); + expect( + decodeClientSettings(encodeClientSettings(decodeClientSettings(patch))) + .sidebarSectionDividersEnabled, + ).toBe(enabled); + } + }); +}); + describe("ClientSettings quit confirmation", () => { it.each([ [true, "hold"], diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 16eb51aa1769c..070f8c53e8479 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -488,6 +488,9 @@ export const ClientSettingsSchema = Schema.Struct({ // the boring sidebar; the boring sidebar toggle clears it. legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), sidebarWorkingShelfEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), + sidebarSectionDividersEnabled: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(false)), + ), sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), ), @@ -1731,6 +1734,7 @@ export const ClientSettingsPatch = Schema.Struct({ threadDetailsItems: Schema.optionalKey(Schema.Record(Schema.String, Schema.Boolean)), legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), sidebarWorkingShelfEnabled: Schema.optionalKey(Schema.Boolean), + sidebarSectionDividersEnabled: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), sidebarProjectGroupingOverrides: Schema.optionalKey( Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode),