UI/chat polish upstream#2281
Conversation
There was a problem hiding this comment.
Pull request overview
This PR polishes the chat UI by introducing compact token usage formatting, adding independently resizable left/right sidebars with persistent width, and improving chat scrolling/composer ergonomics.
Changes:
- Added token formatting + model context-window estimation utilities (with tests) and surfaced them in token usage UI.
- Implemented persistent, draggable sidebar resizing for both sessions (left) and agent details (right) sidebars.
- Improved chat interface ergonomics: “jump to latest”, smarter auto-scroll behavior, and auto-growing message composer.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/lib/tokenUtils.ts | Adds token formatting and model context-window lookup helpers. |
| ui/src/lib/tests/tokenUtils.test.ts | Adds unit tests for the new token utilities. |
| ui/src/hooks/useSidebarWidth.ts | New hook for persistent, clamped sidebar width via localStorage. |
| ui/src/components/ui/sidebar.tsx | Adds per-sidebar width override and disables width transitions while dragging. |
| ui/src/components/ui/scroll-area.tsx | Tweaks scroll behavior/appearance (overlay scrollbar behavior). |
| ui/src/components/sidebars/SidebarResizeHandle.tsx | Adds pointer-driven resize handle used by sidebars. |
| ui/src/components/sidebars/SessionsSidebar.tsx | Wires left sidebar to persistent width + resize handle. |
| ui/src/components/sidebars/AgentDetailsSidebar.tsx | Wires right sidebar to persistent width + resize handle. |
| ui/src/components/chat/TokenStatsTooltip.tsx | Displays compact token counts alongside raw counts in tooltip. |
| ui/src/components/chat/TokenStats.tsx | Shows compact token counts and adds an approximate context usage meter. |
| ui/src/components/chat/ChatMessage.tsx | Improves wrapping behavior for message content (break-words). |
| ui/src/components/chat/ChatLayoutUI.tsx | Passes model/provider/config info into chat context. |
| ui/src/components/chat/ChatInterface.tsx | Adds near-bottom tracking, “Latest” jump button, and auto-growing composer. |
| ui/src/components/chat/ChatAgentContext.tsx | Extends chat context to include model info for token/context UI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function formatTokens(count: number): string { | ||
| if (!Number.isFinite(count) || count < 0) return "0"; | ||
| if (count >= 1_000_000) return `${trimDecimal(count / 1_000_000)}M`; | ||
| if (count >= 1_000) return `${trimDecimal(count / 1_000)}k`; | ||
| return String(Math.round(count)); | ||
| } |
| export function useSidebarWidth(storageKey: string, defaultWidth: number) { | ||
| const [width, setWidthState] = useState<number>(defaultWidth); |
| const reset = useCallback(() => { | ||
| setWidthState(defaultWidth); | ||
| try { |
| const onMove = (event: PointerEvent) => { | ||
| const next = side === "left" ? event.clientX : window.innerWidth - event.clientX; | ||
| onResize(next); | ||
| }; | ||
| const onUp = (event: PointerEvent) => { | ||
| target.releasePointerCapture(event.pointerId); | ||
| target.removeEventListener("pointermove", onMove); | ||
| target.removeEventListener("pointerup", onUp); | ||
| }; | ||
| target.addEventListener("pointermove", onMove); | ||
| target.addEventListener("pointerup", onUp); | ||
| }, |
|
hi @Feelings0220 - thanks for your PR! Could you resolve the conflicts, address the comments and make sure you sign the DCO? |
| className={cn( | ||
| "relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear", | ||
| "relative w-[--sidebar-width] bg-transparent", | ||
| width ? "transition-none" : "transition-[width] duration-200 ease-linear", |
There was a problem hiding this comment.
Both sidebars now always pass width, so this branch is always transition-none and collapsing the sidebar snaps instead of sliding, maybe disable the transition only while a drag is active.
- Auto-growing textarea (1 row min, ~8 rows max) replaces the fixed min-h-[100px] composer; send/voice/cancel buttons are inlined so the idle composer height drops from ~220px to ~60px - Enter sends the message (Shift+Enter for newline), with an IME composition guard so CJK input confirmation doesn't trigger a send - Status/token row only renders while active instead of permanently - Messages column is centered at max-w-3xl; break-all -> break-words; reduced vertical padding - Auto-scroll only follows when the user is near the bottom, with a floating 'Latest' button to jump back down; sending always re-engages follow scrolling Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016d9SxeefoRSLZmQ1oKo17j Signed-off-by: Feelings0220 <124495386+Feelings0220@users.noreply.github.com>
…er, resizable sidebars - ScrollArea switches to Radix type=scroll: the scrollbar overlays content, appears while scrolling, and fades out 600ms later; thinner thumb, no track border (applies to every ScrollArea in the app) - Token counts render in k/M units via formatTokens (exact values stay in tooltips/titles) - The composer usage row gains an approximate context-window meter: 'ctx 12.3k/200k' with a progress bar (amber >80%, red >95%), sized from the latest turn's prompt tokens against a model-prefix window table (unknown models hide the meter). ChatAgentContext gains a modelInfo value (model/provider/ModelConfig ref) provided by ChatLayoutUI so chat components can read the active model. - Chat scroll area top padding reduced (py-6 -> pt-2 pb-6) - Both sidebars are now resizable: Sidebar gains a width override prop (set on the outer wrapper so the layout gap tracks the panel, with width transitions disabled while overridden), a pointer-capture drag handle on the inner edge, 200-480px clamp, per-side localStorage persistence, and double-click to reset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016d9SxeefoRSLZmQ1oKo17j Signed-off-by: Feelings0220 <124495386+Feelings0220@users.noreply.github.com>
- formatTokens: 999950..999999 rounded up to '1000k'; roll to '1M' instead (with a regression test) - useSidebarWidth: a missing localStorage key made Number(null)===0 fall through the range guard by luck; read the raw value and treat missing as NaN explicitly - useSidebarWidth: clamp reset() to the min/max range in case a caller's default drifts outside it - SidebarResizeHandle: also end the drag on pointercancel (touch scroll takeover, alt-tab) and guard releasePointerCapture so an already- released capture doesn't throw Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016d9SxeefoRSLZmQ1oKo17j Signed-off-by: Feelings0220 <124495386+Feelings0220@users.noreply.github.com>
Addresses review feedback: both sidebars always pass a width, so the transition-none branch was always taken and collapsing snapped instead of sliding. Track an isResizing flag in useSidebarWidth (set by the resize handle's pointerdown/up) and gate the transition on that instead, so dragging still tracks the pointer 1:1 while collapse animates. Signed-off-by: Feelings0220 <124495386+Feelings0220@users.noreply.github.com>
b20dc63 to
510d49e
Compare
Optimized the layout with a more compact chat interface, an adjustable sidebar, and other UI improvements.
对话框紧凑,侧边栏可调节等等agent对话布局的优化