Skip to content

Commit a409be5

Browse files
Travisuncursoragent
andcommitted
fix: polish workspace layout, settings exit, and selection toolbar chrome.
Auto-collapse the right panel when the window narrows, unify settings back navigation with drawer-style menus, and tighten toolbar button active states and compact padding. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d8645dd commit a409be5

10 files changed

Lines changed: 177 additions & 66 deletions

File tree

client-ui/src/chat/ChatApp.tsx

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,38 @@ export default function ChatApp() {
128128
return window.innerWidth >= DESKTOP_SIDEBAR_EXPAND_THRESHOLD
129129
})
130130

131+
const {
132+
current: view,
133+
canGoBack,
134+
canGoForward,
135+
navigate,
136+
goBack,
137+
goForward,
138+
} = useAppNavigation('chat')
139+
140+
const electronChrome = isElectron() && !isMobile
141+
const splitEnabled = !isMobile && view === 'chat'
142+
143+
const {
144+
workspaceRef,
145+
rightPanelOpen: rightPanelVisible,
146+
chatVisible,
147+
rightPanelWidth,
148+
showSplitter,
149+
chatWidth,
150+
isDragging,
151+
canToggleChatColumn,
152+
beginDrag,
153+
collapseRightPanel,
154+
toggleRightPanel: handleToggleRightPanel,
155+
toggleChatColumn: handleToggleChatColumn,
156+
} = useWorkspaceSplit({ enabled: splitEnabled })
157+
131158
const collapseSidebars = useCallback(() => {
132159
setSidebarVisible(false)
133160
setSettingsSidebarVisible(false)
134-
}, [setSidebarVisible])
161+
collapseRightPanel(true)
162+
}, [collapseRightPanel, setSidebarVisible])
135163

136164
const expandSidebars = useCallback(() => {
137165
setSidebarVisible(true)
@@ -140,15 +168,6 @@ export default function ChatApp() {
140168

141169
useSidebarResizeSync(!isMobile, collapseSidebars, expandSidebars)
142170

143-
const {
144-
current: view,
145-
canGoBack,
146-
canGoForward,
147-
navigate,
148-
goBack,
149-
goForward,
150-
} = useAppNavigation('chat')
151-
152171
const handleToggleSidebar = useCallback(() => {
153172
if (view === 'settings') {
154173
setSettingsSidebarVisible(prev => !prev)
@@ -170,23 +189,6 @@ export default function ChatApp() {
170189
const [llmLabel, setLlmLabel] = useState('连接中…')
171190
const [backendOk, setBackendOk] = useState(false)
172191

173-
const electronChrome = isElectron() && !isMobile
174-
const splitEnabled = !isMobile && view === 'chat'
175-
176-
const {
177-
workspaceRef,
178-
rightPanelOpen: rightPanelVisible,
179-
chatVisible,
180-
rightPanelWidth,
181-
showSplitter,
182-
chatWidth,
183-
isDragging,
184-
canToggleChatColumn,
185-
beginDrag,
186-
toggleRightPanel: handleToggleRightPanel,
187-
toggleChatColumn: handleToggleChatColumn,
188-
} = useWorkspaceSplit({ enabled: splitEnabled })
189-
190192
const refreshModels = useCallback(async () => {
191193
try {
192194
const { models } = await listAvailableModels()
@@ -258,6 +260,10 @@ export default function ChatApp() {
258260
navigate('settings')
259261
}
260262

263+
const handleExitSettings = useCallback(() => {
264+
navigate('chat')
265+
}, [navigate])
266+
261267
const handleNew = async () => {
262268
try {
263269
const { session } = await createSession()
@@ -484,12 +490,12 @@ export default function ChatApp() {
484490
showSidebarToggle={!isSettings || sidebarOverlayMode}
485491
sidebarHoverReveal={sidebarOverlayMode}
486492
onRevealSidebar={handleEdgeRevealSidebar}
487-
canGoBack={canGoBack}
488-
canGoForward={canGoForward}
493+
canGoBack={!isSettings && canGoBack}
494+
canGoForward={!isSettings && canGoForward}
489495
onToggleSidebar={handleToggleSidebar}
490496
onNewChat={handleNew}
491-
onGoBack={goBack}
492-
onGoForward={goForward}
497+
onGoBack={!isSettings ? goBack : undefined}
498+
onGoForward={!isSettings ? goForward : undefined}
493499
rightPanelOpen={!isSettings ? rightPanelVisible : undefined}
494500
chatColumnVisible={!isSettings ? chatVisible : undefined}
495501
onToggleRightPanel={!isSettings && !isMobile ? handleToggleRightPanel : undefined}
@@ -513,7 +519,7 @@ export default function ChatApp() {
513519
isMobile={isMobile}
514520
sidebarVisible={settingsSidebarVisible}
515521
onSidebarClose={() => setSettingsSidebarVisible(false)}
516-
onBack={goBack}
522+
onBack={handleExitSettings}
517523
onSaved={async () => {
518524
await refreshHealth()
519525
}}

client-ui/src/chat/ChatView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ export default function ChatView({
425425
<ChromeToolButton
426426
label={rightPanelOpen ? '收起右侧面板' : '展开右侧面板'}
427427
iconPadding={DESKTOP_SIDEBAR_TOOL_ICON_PADDING}
428+
active={rightPanelOpen}
428429
onClick={onToggleRightPanel}
429430
>
430431
{rightPanelOpen

client-ui/src/chat/MessageSelectionToolbar.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ const useStyles = makeStyles({
3939
zIndex: 20,
4040
display: 'flex',
4141
flexDirection: 'column',
42-
alignItems: 'stretch',
42+
alignItems: 'flex-start',
4343
width: 'fit-content',
44-
minWidth: '148px',
4544
maxWidth: 'min(400px, calc(100vw - 24px))',
4645
gap: '2px',
4746
padding: '2px',
@@ -57,6 +56,10 @@ const useStyles = makeStyles({
5756
transitionDuration: motion.normal,
5857
transitionTimingFunction: motion.easeOut,
5958
},
59+
toolbarExpanded: {
60+
alignItems: 'stretch',
61+
minWidth: '240px',
62+
},
6063
toolbarConversation: {
6164
minWidth: '280px',
6265
maxWidth: 'min(400px, calc(100vw - 24px))',
@@ -365,10 +368,16 @@ export default function MessageSelectionToolbar({
365368
}
366369

367370
const inConversation = mode === 'conversation'
371+
const isExpanded = mode !== 'compact'
368372

369373
return (
370374
<div
371-
className={mergeClasses(s.toolbar, inConversation && s.toolbarConversation, className)}
375+
className={mergeClasses(
376+
s.toolbar,
377+
isExpanded && s.toolbarExpanded,
378+
inConversation && s.toolbarConversation,
379+
className,
380+
)}
372381
style={style}
373382
role="toolbar"
374383
aria-label="选区工具"

client-ui/src/desktop/ChromeToolButton.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,39 @@ const useStyles = makeStyles({
3131
},
3232
},
3333
},
34+
btnActive: {
35+
backgroundColor: innoTokens.accentSoft,
36+
color: innoTokens.accent,
37+
':hover': {
38+
backgroundColor: innoTokens.accentSoft,
39+
color: innoTokens.accent,
40+
},
41+
},
3442
})
3543

3644
interface ChromeToolButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
3745
children: ReactNode
3846
label: string
3947
/** Inner padding — smaller values leave room for a larger glyph in the same hit target */
4048
iconPadding?: number
49+
active?: boolean
4150
}
4251

4352
export default function ChromeToolButton({
4453
children,
4554
label,
4655
className,
4756
iconPadding = DESKTOP_TOOL_ICON_PADDING,
57+
active = false,
4858
style,
4959
...rest
5060
}: ChromeToolButtonProps) {
5161
const s = useStyles()
5262
return (
5363
<button
5464
type="button"
55-
className={mergeClasses(s.btn, 'inno-focusable', className)}
65+
className={mergeClasses(s.btn, active && s.btnActive, 'inno-focusable', className)}
66+
aria-pressed={active || undefined}
5667
aria-label={label}
5768
title={label}
5869
style={{ padding: `${iconPadding}px`, ...style }}

client-ui/src/desktop/DesktopWindowChrome.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,17 @@ export default function DesktopWindowChrome({
183183
: <PanelLeftExpandRegular fontSize={DESKTOP_SIDEBAR_TOOL_ICON_SIZE} />}
184184
</ChromeToolButton>
185185
)}
186-
{onGoBack && (
186+
{!isSettings && onGoBack && (
187187
<ChromeToolButton label="后退" disabled={!canGoBack} onClick={onGoBack}>
188188
<ArrowLeftRegular fontSize={DESKTOP_TOOL_ICON_SIZE} />
189189
</ChromeToolButton>
190190
)}
191-
{onGoForward && (
191+
{!isSettings && onGoForward && (
192192
<ChromeToolButton label="前进" disabled={!canGoForward} onClick={onGoForward}>
193193
<ArrowRightRegular fontSize={DESKTOP_TOOL_ICON_SIZE} />
194194
</ChromeToolButton>
195195
)}
196-
{!isSettings && onNewChat && (
196+
{!isSettings && onNewChat && !sidebarOpen && (
197197
<ChromeToolButton label="新建对话" onClick={onNewChat}>
198198
<ChatAddRegular fontSize={DESKTOP_TOOL_ICON_SIZE} />
199199
</ChromeToolButton>
@@ -221,6 +221,7 @@ export default function DesktopWindowChrome({
221221
<ChromeToolButton
222222
label={rightPanelOpen ? '收起右侧面板' : '展开右侧面板'}
223223
iconPadding={DESKTOP_SIDEBAR_TOOL_ICON_PADDING}
224+
active={rightPanelOpen}
224225
onClick={onToggleRightPanel}
225226
>
226227
{rightPanelOpen

client-ui/src/desktop/constants.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ export const WORKSPACE_RIGHT_PANEL_DEFAULT_WIDTH = 456
4747
export const WORKSPACE_RIGHT_PANEL_MIN_WIDTH = 228
4848
export const WORKSPACE_SPLITTER_WIDTH = 5
4949

50+
/** Inline left sidebar width — keep in sync with innoTokens.sidebarWidthPx */
51+
export const SIDEBAR_INLINE_WIDTH = 228
52+
53+
/**
54+
* Minimum workspace width (chat area) to keep chat + splitter + right panel open.
55+
* Below this, the right panel auto-collapses so chat keeps a usable 350px column.
56+
*/
57+
export const WORKSPACE_CHAT_RIGHT_MIN_WIDTH =
58+
WORKSPACE_CHAT_MIN_WIDTH + WORKSPACE_SPLITTER_WIDTH + WORKSPACE_RIGHT_PANEL_MIN_WIDTH
59+
60+
/**
61+
* Minimum window width for three inline columns (left sidebar + chat + right panel).
62+
* Matches SIDEBAR_INLINE_WIDTH + WORKSPACE_CHAT_RIGHT_MIN_WIDTH.
63+
*/
64+
export const WORKSPACE_TRIPLE_COLUMN_MIN_WIDTH =
65+
SIDEBAR_INLINE_WIDTH + WORKSPACE_CHAT_RIGHT_MIN_WIDTH
66+
67+
/** Hysteresis buffer so right panel does not flicker at the collapse boundary. */
68+
export const WORKSPACE_PANEL_HYSTERESIS = 28
69+
70+
/** Auto-restore right panel once workspace grows past collapse minimum + hysteresis. */
71+
export const WORKSPACE_RIGHT_PANEL_RESTORE_WIDTH =
72+
WORKSPACE_CHAT_RIGHT_MIN_WIDTH + WORKSPACE_PANEL_HYSTERESIS
73+
5074
/** Title bar stacking — overlay sidebar sits between title and toolbar */
5175
export const DESKTOP_Z_TITLE = 1100
5276
export const DESKTOP_Z_OVERLAY_SIDEBAR = 1150

client-ui/src/hooks/useWorkspaceSplit.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useCallback, useEffect, useRef, useState } from 'react'
22
import {
33
WORKSPACE_CHAT_MIN_WIDTH,
4+
WORKSPACE_CHAT_RIGHT_MIN_WIDTH,
45
WORKSPACE_RIGHT_PANEL_DEFAULT_WIDTH,
56
WORKSPACE_RIGHT_PANEL_MIN_WIDTH,
7+
WORKSPACE_RIGHT_PANEL_RESTORE_WIDTH,
68
WORKSPACE_SPLITTER_WIDTH,
79
} from '../desktop/constants'
810

@@ -30,6 +32,7 @@ export function useWorkspaceSplit({
3032
const [isDragging, setIsDragging] = useState(false)
3133
const savedRightWidthRef = useRef(defaultRightWidth)
3234
const rightPanelWidthRef = useRef(defaultRightWidth)
35+
const autoCollapsedByWidthRef = useRef(false)
3336
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null)
3437

3538
useEffect(() => {
@@ -56,12 +59,22 @@ export function useWorkspaceSplit({
5659
? workspaceWidth
5760
: 0
5861

62+
const canFitRightPanel = workspaceWidth <= 0 || workspaceWidth >= WORKSPACE_CHAT_RIGHT_MIN_WIDTH
63+
5964
const commitWidth = useCallback((nextWidth: number, wsWidth: number) => {
6065
const clamped = clampRightWidth(nextWidth, wsWidth)
6166
savedRightWidthRef.current = clamped
6267
setRightPanelWidth(clamped)
6368
}, [])
6469

70+
const collapseRightPanel = useCallback((markAuto = true) => {
71+
if (!rightPanelOpen) return
72+
savedRightWidthRef.current = rightPanelWidthRef.current
73+
if (markAuto) autoCollapsedByWidthRef.current = true
74+
setRightPanelOpen(false)
75+
setChatVisible(true)
76+
}, [rightPanelOpen])
77+
6578
const beginDrag = useCallback((clientX: number) => {
6679
if (!enabled || !chatVisible || !rightPanelOpen) return
6780
dragRef.current = { startX: clientX, startWidth: rightPanelWidthRef.current }
@@ -110,24 +123,55 @@ export function useWorkspaceSplit({
110123

111124
useEffect(() => {
112125
if (!enabled || isDragging || workspaceWidth <= 0) return
113-
if (!chatVisible || !rightPanelOpen) return
126+
127+
if (rightPanelOpen && workspaceWidth < WORKSPACE_CHAT_RIGHT_MIN_WIDTH) {
128+
collapseRightPanel(true)
129+
return
130+
}
131+
132+
if (
133+
!rightPanelOpen
134+
&& autoCollapsedByWidthRef.current
135+
&& workspaceWidth >= WORKSPACE_RIGHT_PANEL_RESTORE_WIDTH
136+
) {
137+
autoCollapsedByWidthRef.current = false
138+
setRightPanelWidth(savedRightWidthRef.current || defaultRightWidth)
139+
setRightPanelOpen(true)
140+
setChatVisible(true)
141+
return
142+
}
143+
144+
if (!rightPanelOpen || !chatVisible) return
114145

115146
const clamped = clampRightWidth(rightPanelWidth, workspaceWidth)
116147
if (clamped !== rightPanelWidth) {
117148
commitWidth(clamped, workspaceWidth)
118149
}
119-
}, [chatVisible, commitWidth, enabled, isDragging, rightPanelOpen, rightPanelWidth, workspaceWidth])
150+
}, [
151+
chatVisible,
152+
collapseRightPanel,
153+
commitWidth,
154+
defaultRightWidth,
155+
enabled,
156+
isDragging,
157+
rightPanelOpen,
158+
rightPanelWidth,
159+
workspaceWidth,
160+
])
120161

121162
const toggleRightPanel = useCallback(() => {
122163
if (rightPanelOpen) {
123164
savedRightWidthRef.current = rightPanelWidthRef.current
165+
autoCollapsedByWidthRef.current = false
124166
setRightPanelOpen(false)
125167
setChatVisible(true)
126168
return
127169
}
170+
if (enabled && workspaceWidth > 0 && workspaceWidth < WORKSPACE_CHAT_RIGHT_MIN_WIDTH) return
171+
autoCollapsedByWidthRef.current = false
128172
setRightPanelWidth(savedRightWidthRef.current || defaultRightWidth)
129173
setRightPanelOpen(true)
130-
}, [defaultRightWidth, rightPanelOpen])
174+
}, [defaultRightWidth, enabled, rightPanelOpen, workspaceWidth])
131175

132176
const toggleChatColumn = useCallback(() => {
133177
if (!rightPanelOpen) return
@@ -152,7 +196,9 @@ export function useWorkspaceSplit({
152196
chatWidth,
153197
isDragging,
154198
canToggleChatColumn,
199+
canFitRightPanel,
155200
beginDrag,
201+
collapseRightPanel,
156202
toggleRightPanel,
157203
toggleChatColumn,
158204
}

0 commit comments

Comments
 (0)