| author | |
| committer | |
| log | 03728361aa7beb9c13da320097450e6fe65aac3e |
| tree | 217ec6a5888b3ff2789b137bec2e99d7b0e70ca1 |
| parent | 493fbb58870c912b9cb2ba6c2f1dae938877a59a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Code <noreply@anthropic.com>10 files changed, 163 insertions(+), 18 deletions(-)
apps/desktop/src/settings/DesktopClientSettings.test.ts+2| ... | ... | @@ -28,6 +28,8 @@ const clientSettings: ClientSettings = { |
| 28 | 28 | confirmThreadUnpin: false, |
| 29 | 29 | continueThreadsAfterServerUpdate: true, |
| 30 | 30 | contextWindowMeterEnabled: false, |
| 31 | composerCollapseOnBlur: false, | |
| 32 | composerCollapseOnScroll: true, | |
| 31 | 33 | dismissedProviderUpdateNotificationKeys: [], |
| 32 | 34 | diffIgnoreWhitespace: true, |
| 33 | 35 | diffLayout: "stacked", |
apps/web/src/components/chat/ChatComposer.tsx+12-10| ... | ... | @@ -3542,8 +3542,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3542 | 3542 | const isComposerResting = shouldUseRestingComposerLayout({ |
| 3543 | 3543 | isExistingThread: routeKind === "server" && activeThreadId !== null, |
| 3544 | 3544 | isMobileViewport, |
| 3545 | isFocused: isComposerFocused && !isComposerScrollCollapsed, | |
| 3545 | isFocused: isComposerFocused, | |
| 3546 | isScrollCollapsed: isComposerScrollCollapsed, | |
| 3546 | 3547 | hasExpandedChrome: composerHasExpandedChrome, |
| 3548 | collapseOnBlur: settings.composerCollapseOnBlur, | |
| 3547 | 3549 | }); |
| 3548 | 3550 | // The relocated controls live in the context strip whenever the composer is |
| 3549 | 3551 | // collapsed for any reason, the desktop resting layout or the phone |
| ... | ... | @@ -3615,8 +3617,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3615 | 3617 | const canTrackComposerScrollGesture = |
| 3616 | 3618 | routeKind === "server" && activeThreadId !== null && !isMobileViewport; |
| 3617 | 3619 | const canScrollCollapseComposer = |
| 3618 | canTrackComposerScrollGesture && !composerHasExpandedChrome && !showInlineTasksBadge; | |
| 3619 | composerScrollCollapseEligibleRef.current = canScrollCollapseComposer; | |
| 3620 | canTrackComposerScrollGesture && | |
| 3621 | settings.composerCollapseOnScroll && | |
| 3622 | !composerHasExpandedChrome && | |
| 3623 | !showInlineTasksBadge; | |
| 3624 | // Scrolling only has something to collapse while the composer is expanded. | |
| 3625 | // With blur collapse off that includes an unfocused composer, so the wheel | |
| 3626 | // handler keys off this rather than editor focus. | |
| 3627 | composerScrollCollapseEligibleRef.current = canScrollCollapseComposer && !isComposerResting; | |
| 3620 | 3628 | |
| 3621 | 3629 | useEffect(() => { |
| 3622 | 3630 | if (!canScrollCollapseComposer) { |
| ... | ... | @@ -3657,11 +3665,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3657 | 3665 | resetComposerScrollGesture(composerScrollGestureRef.current); |
| 3658 | 3666 | }; |
| 3659 | 3667 | const handleTimelineWheel = (event: WheelEvent) => { |
| 3660 | const activeElement = document.activeElement; | |
| 3661 | const isPromptEditorFocused = | |
| 3662 | activeElement instanceof HTMLElement && | |
| 3663 | activeElement.isContentEditable && | |
| 3664 | composerFormRef.current?.contains(activeElement) === true; | |
| 3665 | 3668 | if (event.ctrlKey || !(event.target instanceof Element)) { |
| 3666 | 3669 | return; |
| 3667 | 3670 | } |
| ... | ... | @@ -3695,8 +3698,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3695 | 3698 | now: window.performance.now(), |
| 3696 | 3699 | deltaPx, |
| 3697 | 3700 | collapseThresholdPx: COMPOSER_SCROLL_COLLAPSE_THRESHOLD_PX, |
| 3698 | collapseEligible: | |
| 3699 | targetsTimeline && composerScrollCollapseEligibleRef.current && isPromptEditorFocused, | |
| 3701 | collapseEligible: targetsTimeline && composerScrollCollapseEligibleRef.current, | |
| 3700 | 3702 | canScrollInGestureDirection, |
| 3701 | 3703 | scrollsTowardLogicalEnd: event.deltaY > 0 && isTimelineAtLogicalEnd(), |
| 3702 | 3704 | }); |
apps/web/src/components/composerFooterLayout.test.ts+23| ... | ... | @@ -79,13 +79,36 @@ describe("shouldUseRestingComposerLayout", () => { |
| 79 | 79 | isExistingThread: true, |
| 80 | 80 | isMobileViewport: false, |
| 81 | 81 | isFocused: false, |
| 82 | isScrollCollapsed: false, | |
| 82 | 83 | hasExpandedChrome: false, |
| 84 | collapseOnBlur: true, | |
| 83 | 85 | }; |
| 84 | 86 | |
| 85 | 87 | it("uses the resting layout for an unfocused desktop composer", () => { |
| 86 | 88 | expect(shouldUseRestingComposerLayout(resting)).toBe(true); |
| 87 | 89 | }); |
| 88 | 90 | |
| 91 | it("keeps an unfocused composer expanded when blur collapse is off", () => { | |
| 92 | expect(shouldUseRestingComposerLayout({ ...resting, collapseOnBlur: false })).toBe(false); | |
| 93 | }); | |
| 94 | ||
| 95 | it("rests a scroll-collapsed composer even while focused", () => { | |
| 96 | expect( | |
| 97 | shouldUseRestingComposerLayout({ ...resting, isFocused: true, isScrollCollapsed: true }), | |
| 98 | ).toBe(true); | |
| 99 | }); | |
| 100 | ||
| 101 | it("rests a scroll-collapsed composer regardless of the blur preference", () => { | |
| 102 | expect( | |
| 103 | shouldUseRestingComposerLayout({ | |
| 104 | ...resting, | |
| 105 | isFocused: true, | |
| 106 | isScrollCollapsed: true, | |
| 107 | collapseOnBlur: false, | |
| 108 | }), | |
| 109 | ).toBe(true); | |
| 110 | }); | |
| 111 | ||
| 89 | 112 | it("keeps new-thread composers expanded", () => { |
| 90 | 113 | expect(shouldUseRestingComposerLayout({ ...resting, isExistingThread: false })).toBe(false); |
| 91 | 114 | }); |
apps/web/src/components/composerFooterLayout.ts+9-6| ... | ... | @@ -27,7 +27,9 @@ export function shouldUseRestingComposerLayout(input: { |
| 27 | 27 | isExistingThread: boolean; |
| 28 | 28 | isMobileViewport: boolean; |
| 29 | 29 | isFocused: boolean; |
| 30 | isScrollCollapsed: boolean; | |
| 30 | 31 | hasExpandedChrome: boolean; |
| 32 | collapseOnBlur: boolean; | |
| 31 | 33 | }): boolean { |
| 32 | 34 | // Passive draft content is deliberately absent here. Resting only clamps |
| 33 | 35 | // the prompt row and overlays its actions; non-image attachment and context |
| ... | ... | @@ -37,12 +39,13 @@ export function shouldUseRestingComposerLayout(input: { |
| 37 | 39 | // deliberately absent here: resting reclaims vertical space at every |
| 38 | 40 | // desktop width, and where the strip is missing or too narrow the controls |
| 39 | 41 | // simply return when the composer is focused. |
| 40 | return ( | |
| 41 | input.isExistingThread && | |
| 42 | !input.isMobileViewport && | |
| 43 | !input.isFocused && | |
| 44 | !input.hasExpandedChrome | |
| 45 | ); | |
| 42 | // | |
| 43 | // A scroll collapse rests the composer regardless of the blur preference: | |
| 44 | // the user asked for it with the gesture, and it lifts on the next | |
| 45 | // composer interaction. With blur collapse off, losing focus alone never | |
| 46 | // rests the composer. | |
| 47 | const collapsed = input.isScrollCollapsed || (input.collapseOnBlur && !input.isFocused); | |
| 48 | return input.isExistingThread && !input.isMobileViewport && collapsed && !input.hasExpandedChrome; | |
| 46 | 49 | } |
| 47 | 50 | |
| 48 | 51 | export function shouldAnimateComposerRestingTransition(input: { |
apps/web/src/components/settings/SettingsPanels.tsx+71| ... | ... | @@ -172,6 +172,12 @@ const TIMESTAMP_FORMAT_LABELS = { |
| 172 | 172 | "24-hour": "24-hour", |
| 173 | 173 | } as const; |
| 174 | 174 | |
| 175 | const COMPOSER_COLLAPSE_TRIGGER_LABELS = { | |
| 176 | blur: "On unfocus", | |
| 177 | scroll: "On scroll", | |
| 178 | } as const; | |
| 179 | type ComposerCollapseTrigger = keyof typeof COMPOSER_COLLAPSE_TRIGGER_LABELS; | |
| 180 | ||
| 175 | 181 | const DIFF_LAYOUT_LABELS: Record<DiffLayout, string> = { |
| 176 | 182 | stacked: "Stacked", |
| 177 | 183 | split: "Split", |
| ... | ... | @@ -542,6 +548,10 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 542 | 548 | ...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu |
| 543 | 549 | ? ["Show skills in slash menu"] |
| 544 | 550 | : []), |
| 551 | ...(settings.composerCollapseOnBlur !== DEFAULT_UNIFIED_SETTINGS.composerCollapseOnBlur || | |
| 552 | settings.composerCollapseOnScroll !== DEFAULT_UNIFIED_SETTINGS.composerCollapseOnScroll | |
| 553 | ? ["Collapse composer"] | |
| 554 | : []), | |
| 545 | 555 | ...(settings.contextWindowMeterEnabled !== DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled |
| 546 | 556 | ? ["Context window indicator"] |
| 547 | 557 | : []), |
| ... | ... | @@ -599,6 +609,8 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 599 | 609 | settings.confirmThreadArchive, |
| 600 | 610 | settings.confirmThreadDelete, |
| 601 | 611 | settings.confirmThreadUnpin, |
| 612 | settings.composerCollapseOnBlur, | |
| 613 | settings.composerCollapseOnScroll, | |
| 602 | 614 | settings.addProjectBaseDirectory, |
| 603 | 615 | settings.defaultThreadEnvMode, |
| 604 | 616 | settings.newWorktreesStartFromOrigin, |
| ... | ... | @@ -703,6 +715,8 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 703 | 715 | diffLayout: DEFAULT_UNIFIED_SETTINGS.diffLayout, |
| 704 | 716 | proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled, |
| 705 | 717 | showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu, |
| 718 | composerCollapseOnBlur: DEFAULT_UNIFIED_SETTINGS.composerCollapseOnBlur, | |
| 719 | composerCollapseOnScroll: DEFAULT_UNIFIED_SETTINGS.composerCollapseOnScroll, | |
| 706 | 720 | contextWindowMeterEnabled: DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled, |
| 707 | 721 | environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, |
| 708 | 722 | glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, |
| ... | ... | @@ -2010,6 +2024,13 @@ export function GeneralSettingsPanel() { |
| 2010 | 2024 | const serverProviders = useAtomValue(primaryServerProvidersAtom); |
| 2011 | 2025 | const supportsAutoSettlement = |
| 2012 | 2026 | useAtomValue(primaryServerConfigAtom)?.environment.capabilities.threadAutoSettlement === true; |
| 2027 | const composerCollapseTriggers = useMemo<ComposerCollapseTrigger[]>( | |
| 2028 | () => [ | |
| 2029 | ...(settings.composerCollapseOnBlur ? (["blur"] as const) : []), | |
| 2030 | ...(settings.composerCollapseOnScroll ? (["scroll"] as const) : []), | |
| 2031 | ], | |
| 2032 | [settings.composerCollapseOnBlur, settings.composerCollapseOnScroll], | |
| 2033 | ); | |
| 2013 | 2034 | const diagnosticsDescription = formatDiagnosticsDescription({ |
| 2014 | 2035 | localTracingEnabled: observability?.localTracingEnabled ?? false, |
| 2015 | 2036 | otlpTracesEnabled: observability?.otlpTracesEnabled ?? false, |
| ... | ... | @@ -2334,6 +2355,56 @@ export function GeneralSettingsPanel() { |
| 2334 | 2355 | } |
| 2335 | 2356 | /> |
| 2336 | 2357 | |
| 2358 | <SettingsRow | |
| 2359 | {...searchableSetting("composer-collapse")} | |
| 2360 | description="Rest the composer of an existing thread into a single line when it loses focus, when you scroll the conversation, or both. Pick neither to keep it expanded." | |
| 2361 | resetAction={ | |
| 2362 | settings.composerCollapseOnBlur !== DEFAULT_UNIFIED_SETTINGS.composerCollapseOnBlur || | |
| 2363 | settings.composerCollapseOnScroll !== | |
| 2364 | DEFAULT_UNIFIED_SETTINGS.composerCollapseOnScroll ? ( | |
| 2365 | <SettingResetButton | |
| 2366 | label="collapse composer" | |
| 2367 | onClick={() => | |
| 2368 | updateSettings({ | |
| 2369 | composerCollapseOnBlur: DEFAULT_UNIFIED_SETTINGS.composerCollapseOnBlur, | |
| 2370 | composerCollapseOnScroll: DEFAULT_UNIFIED_SETTINGS.composerCollapseOnScroll, | |
| 2371 | }) | |
| 2372 | } | |
| 2373 | /> | |
| 2374 | ) : null | |
| 2375 | } | |
| 2376 | control={ | |
| 2377 | <Select | |
| 2378 | multiple | |
| 2379 | value={composerCollapseTriggers} | |
| 2380 | onValueChange={(next) => | |
| 2381 | updateSettings({ | |
| 2382 | composerCollapseOnBlur: next.includes("blur"), | |
| 2383 | composerCollapseOnScroll: next.includes("scroll"), | |
| 2384 | }) | |
| 2385 | } | |
| 2386 | > | |
| 2387 | <SelectTrigger size="sm" className="w-full sm:w-40" aria-label="Collapse composer"> | |
| 2388 | <SelectValue> | |
| 2389 | {composerCollapseTriggers.length === 0 | |
| 2390 | ? "Never" | |
| 2391 | : composerCollapseTriggers | |
| 2392 | .map((trigger) => COMPOSER_COLLAPSE_TRIGGER_LABELS[trigger]) | |
| 2393 | .join(", ")} | |
| 2394 | </SelectValue> | |
| 2395 | </SelectTrigger> | |
| 2396 | <SelectPopup align="end" alignItemWithTrigger={false}> | |
| 2397 | <SelectItem showCheck value="blur"> | |
| 2398 | {COMPOSER_COLLAPSE_TRIGGER_LABELS.blur} | |
| 2399 | </SelectItem> | |
| 2400 | <SelectItem showCheck value="scroll"> | |
| 2401 | {COMPOSER_COLLAPSE_TRIGGER_LABELS.scroll} | |
| 2402 | </SelectItem> | |
| 2403 | </SelectPopup> | |
| 2404 | </Select> | |
| 2405 | } | |
| 2406 | /> | |
| 2407 | ||
| 2337 | 2408 | <SettingsRow |
| 2338 | 2409 | serverScoped |
| 2339 | 2410 | {...searchableSetting("provider-update-checks")} |
apps/web/src/components/settings/settingsSearch.ts+8| ... | ... | @@ -202,6 +202,14 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 202 | 202 | to: "/settings/general", |
| 203 | 203 | searchTerms: ["command menu dollar $ slash /"], |
| 204 | 204 | }, |
| 205 | { | |
| 206 | id: "composer-collapse", | |
| 207 | title: "Collapse composer", | |
| 208 | to: "/settings/general", | |
| 209 | searchTerms: [ | |
| 210 | "composer rest resting unfocus blur focus click away scroll wheel conversation timeline shrink minimize", | |
| 211 | ], | |
| 212 | }, | |
| 205 | 213 | { |
| 206 | 214 | id: "provider-update-checks", |
| 207 | 215 | title: "Provider update checks", |
apps/web/src/components/ui/select.tsx+14-1| ... | ... | @@ -4,7 +4,7 @@ import { mergeProps } from "@base-ui/react/merge-props"; |
| 4 | 4 | import { Select as SelectPrimitive } from "@base-ui/react/select"; |
| 5 | 5 | import { useRender } from "@base-ui/react/use-render"; |
| 6 | 6 | import { cva, type VariantProps } from "class-variance-authority"; |
| 7 | import { ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon } from "lucide-react"; | |
| 7 | import { CheckIcon, ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon } from "lucide-react"; | |
| 8 | 8 | import type * as React from "react"; |
| 9 | 9 | |
| 10 | 10 | import { cn } from "~/lib/utils"; |
| ... | ... | @@ -180,19 +180,32 @@ function SelectItem({ |
| 180 | 180 | className, |
| 181 | 181 | children, |
| 182 | 182 | hideIndicator: _hideIndicator = false, |
| 183 | showCheck = false, | |
| 183 | 184 | ...props |
| 184 | 185 | }: SelectPrimitive.Item.Props & { |
| 185 | 186 | hideIndicator?: boolean; |
| 187 | /** Render a check mark on selected items. Multi-selects use it so every chosen item is visible at once. */ | |
| 188 | showCheck?: boolean; | |
| 186 | 189 | }) { |
| 187 | 190 | return ( |
| 188 | 191 | <SelectPrimitive.Item |
| 189 | 192 | className={cn( |
| 190 | 193 | "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none data-selected:bg-foreground/[0.08] data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", |
| 194 | showCheck && "gap-2", | |
| 191 | 195 | className, |
| 192 | 196 | )} |
| 193 | 197 | data-slot="select-item" |
| 194 | 198 | {...props} |
| 195 | 199 | > |
| 200 | {showCheck ? ( | |
| 201 | <SelectPrimitive.ItemIndicator | |
| 202 | className="flex w-4 shrink-0 items-center justify-center" | |
| 203 | data-slot="select-item-indicator" | |
| 204 | keepMounted | |
| 205 | > | |
| 206 | <CheckIcon className="in-data-[selected]:opacity-100 opacity-0" /> | |
| 207 | </SelectPrimitive.ItemIndicator> | |
| 208 | ) : null} | |
| 196 | 209 | <SelectPrimitive.ItemText |
| 197 | 210 | className="min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground" |
| 198 | 211 | data-slot="select-item-text" |
docs/user/composer.md+2-1| ... | ... | @@ -56,7 +56,8 @@ the composer loses focus. At wider sizes, scrolling the conversation also rests |
| 56 | 56 | except when scrolling toward the end while already there. When the thread-context strip has room, |
| 57 | 57 | the model and mode controls stay available beside the thread context; otherwise they return when the |
| 58 | 58 | composer is focused. Focus the composer or start typing to expand it again. New-thread layouts keep |
| 59 | the full composer. | |
| 59 | the full composer. **Settings → General → Collapse composer** chooses which triggers rest it: | |
| 60 | **On unfocus**, **On scroll**, both, or neither. With neither selected the composer stays expanded. | |
| 60 | 61 | |
| 61 | 62 | At phone-sized web or desktop window widths, existing threads animate between their compact and |
| 62 | 63 | expanded layouts. Up to three image attachments remain visible in either resting layout, followed |
packages/contracts/src/settings.test.ts+16| ... | ... | @@ -228,6 +228,22 @@ describe("ClientSettings context window meter", () => { |
| 228 | 228 | }); |
| 229 | 229 | }); |
| 230 | 230 | |
| 231 | describe("ClientSettings composer collapse", () => { | |
| 232 | it("collapses on blur and scroll by default and accepts opting out of each", () => { | |
| 233 | const defaults = decodeClientSettings({}); | |
| 234 | expect(defaults.composerCollapseOnBlur).toBe(true); | |
| 235 | expect(defaults.composerCollapseOnScroll).toBe(true); | |
| 236 | ||
| 237 | const blurOff = decodeClientSettings({ composerCollapseOnBlur: false }); | |
| 238 | expect(blurOff.composerCollapseOnBlur).toBe(false); | |
| 239 | expect(blurOff.composerCollapseOnScroll).toBe(true); | |
| 240 | ||
| 241 | expect( | |
| 242 | decodeClientSettingsPatch({ composerCollapseOnScroll: false }).composerCollapseOnScroll, | |
| 243 | ).toBe(false); | |
| 244 | }); | |
| 245 | }); | |
| 246 | ||
| 231 | 247 | describe("ServerSettings thread settlement", () => { |
| 232 | 248 | it("defaults merge settlement on and inactivity settlement to three days", () => { |
| 233 | 249 | const settings = decodeServerSettings({}); |
packages/contracts/src/settings.ts+6| ... | ... | @@ -319,6 +319,10 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 319 | 319 | // Legacy context window meter. The composer hides it by default; users who |
| 320 | 320 | // still want the old usage indicator can restore it from Settings. |
| 321 | 321 | contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 322 | // Desktop resting composer. Each trigger that settles an existing thread's | |
| 323 | // composer into its single-line layout can be turned off on its own. | |
| 324 | composerCollapseOnBlur: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | |
| 325 | composerCollapseOnScroll: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | |
| 322 | 326 | proactivePanelsEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 323 | 327 | showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 324 | 328 | // Legacy sidebar (the original per-project tree). Deliberately a fresh key |
| ... | ... | @@ -1165,6 +1169,8 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 1165 | 1169 | ), |
| 1166 | 1170 | planModeEnabled: Schema.optionalKey(Schema.Boolean), |
| 1167 | 1171 | contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), |
| 1172 | composerCollapseOnBlur: Schema.optionalKey(Schema.Boolean), | |
| 1173 | composerCollapseOnScroll: Schema.optionalKey(Schema.Boolean), | |
| 1168 | 1174 | proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean), |
| 1169 | 1175 | showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), |
| 1170 | 1176 | legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), |