feat(settings): add a scrollbar minimap switch under Appearance

chatMinimapEnabled defaults to on; turning it off restores the native message scrollbar while keeping continuous history and DOM windowing.
This commit is contained in:
mesalogo
2026-10-12 21:26:39 +08:00
parent 4edc34c625
commit b22be5146b
17 changed files with 127 additions and 5 deletions
+15
View File
@@ -1256,6 +1256,7 @@ function installRemoteProjectsSetting(enabled: boolean): {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3486,6 +3487,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3591,6 +3593,7 @@ describe("App", () => {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3608,6 +3611,7 @@ describe("App", () => {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3711,6 +3715,7 @@ describe("App", () => {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3728,6 +3733,7 @@ describe("App", () => {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -6430,6 +6436,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14663,6 +14670,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14680,6 +14688,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14734,6 +14743,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14795,6 +14805,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14833,6 +14844,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14850,6 +14862,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14913,6 +14926,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14973,6 +14987,7 @@ describe("App", () => {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
+4
View File
@@ -6511,6 +6511,8 @@ function App(): React.JSX.Element {
const settingsPanelActions = useStableHandlers({
onTransparentFrostedEffectEnabledChange: (enabled: boolean) =>
updateApplicationSettings({ transparentFrostedEffectEnabled: enabled }),
onChatMinimapEnabledChange: (enabled: boolean) =>
updateApplicationSettings({ chatMinimapEnabled: enabled }),
onRetryApplicationSettings: () => void reloadApplicationSettings(),
onBrandingPreferencesChange: (preferences: Parameters<typeof saveBrandingPreferences>[0]) => {
if (!saveBrandingPreferences(preferences)) {
@@ -7069,6 +7071,7 @@ function App(): React.JSX.Element {
conversationHtmlRenderingEnabled={
conversationHtmlRenderingEnabled
}
chatMinimapEnabled={applicationSettings?.chatMinimapEnabled ?? true}
conversationId={conversationId}
key={conversationId}
loadHistory={ensureConversationHistory}
@@ -7394,6 +7397,7 @@ function App(): React.JSX.Element {
{...settingsPanelActions}
appearanceTheme={appearanceTheme}
transparentFrostedEffectEnabled={applicationSettings?.transparentFrostedEffectEnabled ?? true}
chatMinimapEnabled={applicationSettings?.chatMinimapEnabled ?? true}
applicationSettingsPending={applicationSettingsPending}
applicationSettingsLocked={applicationSettingsUnconfirmed || !applicationSettings}
applicationSettingsError={applicationSettingsError}
+13
View File
@@ -181,6 +181,19 @@ afterEach(() => {
vi.unstubAllGlobals()
})
describe('ChatHistoryPane scrollbar minimap setting', () => {
it('replaces the native scrollbar only while the minimap is enabled', () => {
const messages = makeMessages(5)
const { container, rerender, props } = setup(messages)
const pane = container.querySelector<HTMLElement>('.chat-history-pane')!
expect(pane).toHaveAttribute('data-minimap', 'true')
expect(container.querySelector('.chat-minimap')).not.toBeNull()
rerender(<ChatHistoryPane {...props} chatMinimapEnabled={false} />)
expect(pane).not.toHaveAttribute('data-minimap')
expect(container.querySelector('.chat-minimap')).toBeNull()
})
})
describe('ChatHistoryPane windowing', () => {
it('restores disclosures after eviction by more than eight row interactions with bounded DOM', async () => {
const messages = makeMessages(2_000).map(message => ({ ...message, role: 'assistant' as const, reasoning: 'Reasoning' }))
+5 -1
View File
@@ -152,6 +152,7 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
active,
artifactById,
conversationHtmlRenderingEnabled,
chatMinimapEnabled = true,
conversation,
locale,
onCopyMessage,
@@ -177,6 +178,8 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
active: boolean;
artifactById: ReadonlyMap<string, AssistantArtifact>;
conversationHtmlRenderingEnabled: boolean;
/** Replaces the native scrollbar with the minimap; off restores the scrollbar. */
chatMinimapEnabled?: boolean;
conversation: Conversation;
locale: TimeFormatLocale;
onCopyMessage: (content: string, kind?: 'tool') => Promise<boolean>;
@@ -1071,6 +1074,7 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
className="chat-history-pane"
data-active={active ? "true" : "false"}
data-conversation-id={conversation.id}
data-minimap={chatMinimapEnabled ? "true" : undefined}
hidden={!active}
inert={!active}
>
@@ -1142,7 +1146,7 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
/>
</div>
</section>
{active && (
{active && chatMinimapEnabled && (
<ChatMinimap
messages={messages}
rowHeights={heights}
@@ -79,6 +79,7 @@ const otherTodo: MagicTodoItem = { ...todo, id: '00000000-0000-4000-8000-0000000
const settings: ApplicationSettings = {
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
checkUpdatesOnStartup: false, updateSource: 'github', modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings, applicationNavigation: defaultApplicationNavigation,
localInferenceEnabled: true, deviceSharingEnabled: false, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false,
+28
View File
@@ -520,6 +520,7 @@ let applicationSettings: ApplicationSettings = {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -815,6 +816,7 @@ describe('SettingsPanel runtime files', () => {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -1594,6 +1596,32 @@ describe('SettingsPanel runtime files', () => {
expect(onChange).toHaveBeenLastCalledWith(true)
})
it.each([
['zh-CN', '外观', '滚动条缩略图'],
['en-US', 'Appearance', 'Scrollbar minimap']
] as const)('exposes the scrollbar minimap Switch under Appearance in %s', async (locale, category, label) => {
await changeUiLocale(locale)
const onChange = vi.fn(async () => true)
const props = {
...heartbeatSettingsProps,
open: true,
onClearLocalData: vi.fn(async () => {}),
onClose: vi.fn(),
onSaved: vi.fn(),
onChatMinimapEnabledChange: onChange
}
const { rerender } = render(<SettingsPanel {...props} />)
fireEvent.click(screen.getByRole('tab', { name: category }))
const toggle = screen.getByRole('switch', { name: label })
expect(toggle).toBeChecked()
fireEvent.click(toggle)
expect(onChange).toHaveBeenCalledExactlyOnceWith(false)
rerender(<SettingsPanel {...props} chatMinimapEnabled={false} />)
expect(toggle).not.toBeChecked()
fireEvent.click(toggle)
expect(onChange).toHaveBeenLastCalledWith(true)
})
it.each([
['zh-CN', '平台功能', '桌面通知'],
['en-US', 'Platform features', 'Desktop notifications']
+20
View File
@@ -243,6 +243,8 @@ type SettingsPanelProps = {
applicationSettingsLocked?: boolean
applicationSettingsError?: string
onTransparentFrostedEffectEnabledChange?: (enabled: boolean) => Promise<boolean>
chatMinimapEnabled?: boolean
onChatMinimapEnabledChange?: (enabled: boolean) => Promise<boolean>
onRetryApplicationSettings?: () => void
onAppearanceThemeChange?: (theme: AppearanceTheme) => void
brandingPreferences?: BrandingPreferences
@@ -658,6 +660,8 @@ function SettingsPanelView({
applicationSettingsLocked = false,
applicationSettingsError,
onTransparentFrostedEffectEnabledChange,
chatMinimapEnabled = true,
onChatMinimapEnabledChange,
onRetryApplicationSettings,
onAppearanceThemeChange = () => {},
brandingPreferences = defaultBrandingPreferences,
@@ -2380,6 +2384,22 @@ function SettingsPanelView({
<p id="frosted-glass-description" className="settings-panel__description">
{t('appearance.frostedGlass.description')}
</p>
<label className="toggle-row">
<span>{t('appearance.chatMinimap.label')}</span>
<input
type="checkbox"
role="switch"
checked={chatMinimapEnabled}
disabled={applicationSettingsPending || applicationSettingsLocked || !onChatMinimapEnabledChange}
aria-describedby="chat-minimap-description"
onChange={(event) => {
void onChatMinimapEnabledChange?.(event.target.checked)
}}
/>
</label>
<p id="chat-minimap-description" className="settings-panel__description">
{t('appearance.chatMinimap.description')}
</p>
{applicationSettingsError && (
<div role="alert">
<p>{applicationSettingsError}</p>
@@ -25,6 +25,7 @@ describe('UpdateSettingsSection', () => {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
chatMinimapEnabled: true,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -876,6 +876,10 @@ export const settings = {
description: 'Applies to the top bar and expanded runtime checklist. Takes effect as soon as it is saved.',
retry: 'Retry'
},
chatMinimap: {
label: 'Scrollbar minimap',
description: 'Replaces the message scrollbar with an overview of the conversation that expands on hover; click or drag to jump. Takes effect as soon as it is saved.'
},
theme: {
title: 'Interface theme',
description: 'Applies immediately and is saved on this device',
@@ -811,6 +811,10 @@ export const settings = {
description: '应用于顶栏和展开的运行待办卡片,保存后立即生效。',
retry: '重试'
},
chatMinimap: {
label: '滚动条缩略图',
description: '在消息区右侧用缩略图代替滚动条,悬停时展开,可点击或拖动定位。保存后立即生效。'
},
theme: {
title: '界面主题',
description: '选择后立即应用,并保存在此设备',
+3 -3
View File
@@ -7343,12 +7343,12 @@ button > svg {
color: var(--danger);
}
/* The minimap replaces the native scrollbar of the message scroller. */
.chat-history-pane > .chat {
/* When enabled, the minimap replaces the native scrollbar of the message scroller. */
.chat-history-pane[data-minimap='true'] > .chat {
scrollbar-width: none;
}
.chat-history-pane > .chat::-webkit-scrollbar {
.chat-history-pane[data-minimap='true'] > .chat::-webkit-scrollbar {
display: none;
}