This commit is contained in:
Arjun 2026-06-12 13:58:15 +05:30
parent 3384f0f38f
commit 52772dd8dd
4 changed files with 158 additions and 10 deletions

View file

@ -36,6 +36,7 @@ import { HomeView } from '@/components/home-view';
import { MeetingsView } from '@/components/meetings-view';
import { CodeView, type ActiveCodeSession } from '@/components/code/code-view';
import { CodeChat } from '@/components/code/code-chat';
import { ResizableRightPane } from '@/components/code/resizable-right-pane';
import { SidebarSectionProvider } from '@/contexts/sidebar-context';
import {
Conversation,
@ -6173,10 +6174,9 @@ function App() {
code session it swaps to the direct-drive chat; rowboat-mode
sessions use the regular assistant chat bound to their run. */}
{isRightPaneContext && isCodeOpen && activeCodeSession?.session.mode === 'direct' ? (
<aside
className="flex min-h-0 shrink-0 flex-col border-l bg-background"
style={{ width: DEFAULT_CHAT_PANE_WIDTH }}
onMouseDownCapture={() => setActiveShortcutPane('right')}
<ResizableRightPane
defaultWidth={DEFAULT_CHAT_PANE_WIDTH}
onActivate={() => setActiveShortcutPane('right')}
>
<CodeChat
key={activeCodeSession.session.id}
@ -6184,7 +6184,7 @@ function App() {
status={activeCodeSession.status}
onOpenDiff={setCodeDiffPath}
/>
</aside>
</ResizableRightPane>
) : isRightPaneContext && (
<ChatSidebar
placement={chatPanePlacement}

View file

@ -273,9 +273,14 @@ export function NewSessionDialog({
))}
</SelectContent>
</Select>
<p className="text-[11px] text-muted-foreground">
How the coding agent's file edits and commands get approved applies in both modes.
</p>
</div>
{modelOptions.length > 0 && (
{/* The model only powers Rowboat's own turns; the coding agent uses its
own configured model, so hide this entirely for direct sessions. */}
{mode === 'rowboat' && modelOptions.length > 0 && (
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium">Model</label>
<Select value={modelKey} onValueChange={setModelKey}>

View file

@ -0,0 +1,132 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
// Mirror of ChatSidebar's resize behavior for the direct-mode code chat pane:
// same bounds, same drag handle, and the SAME persisted width key — so the
// assistant pane and the direct pane stay the same size as the user switches
// between session modes.
const MIN_WIDTH = 360
const MAX_WIDTH = 1600
const MIN_MAIN_PANE_WIDTH = 420
const MIN_MAIN_PANE_RATIO = 0.3
const RIGHT_PANE_WIDTH_STORAGE_KEY = 'x:right-pane-width'
function clampPaneWidth(width: number, maxWidth: number = MAX_WIDTH): number {
const boundedMax = Math.max(0, Math.min(MAX_WIDTH, maxWidth))
const boundedMin = Math.min(MIN_WIDTH, boundedMax)
return Math.min(boundedMax, Math.max(boundedMin, width))
}
function readStoredWidth(defaultWidth: number): number {
const fallback = clampPaneWidth(defaultWidth)
if (typeof window === 'undefined') return fallback
try {
const raw = window.localStorage.getItem(RIGHT_PANE_WIDTH_STORAGE_KEY)
if (!raw) return fallback
const parsed = Number(raw)
if (!Number.isFinite(parsed)) return fallback
return clampPaneWidth(parsed)
} catch {
return fallback
}
}
export function ResizableRightPane({
defaultWidth = 460,
className,
children,
onActivate,
}: {
defaultWidth?: number
className?: string
children: React.ReactNode
/** Fired on any mouse-down inside the pane (keyboard-shortcut focus tracking). */
onActivate?: () => void
}) {
const paneRef = useRef<HTMLDivElement>(null)
const [width, setWidth] = useState(() => readStoredWidth(defaultWidth))
const [isResizing, setIsResizing] = useState(false)
const startXRef = useRef(0)
const startWidthRef = useRef(0)
// Never let the pane squeeze the main content below a usable width.
const getMaxAllowedWidth = useCallback(() => {
if (typeof window === 'undefined') return MAX_WIDTH
const paneElement = paneRef.current
const splitContainer = paneElement?.parentElement
const mainPane = splitContainer?.querySelector<HTMLElement>('[data-slot="sidebar-inset"]')
const paneWidth = paneElement?.getBoundingClientRect().width ?? 0
const mainPaneWidth = mainPane?.getBoundingClientRect().width ?? 0
const splitWidth = paneWidth + mainPaneWidth
const fallbackWidth = splitContainer?.clientWidth ?? window.innerWidth
const availableSplitWidth = splitWidth > 0 ? splitWidth : fallbackWidth
const minMainPaneWidth = Math.min(
availableSplitWidth,
Math.max(MIN_MAIN_PANE_WIDTH, Math.floor(availableSplitWidth * MIN_MAIN_PANE_RATIO)),
)
return Math.max(0, availableSplitWidth - minMainPaneWidth)
}, [])
useEffect(() => {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(RIGHT_PANE_WIDTH_STORAGE_KEY, String(width))
} catch {
// keep in-memory width on persistence failure
}
}, [width])
useEffect(() => {
const clampToAvailableWidth = () => {
const maxAllowedWidth = getMaxAllowedWidth()
setWidth((prev) => clampPaneWidth(prev, maxAllowedWidth))
}
clampToAvailableWidth()
window.addEventListener('resize', clampToAvailableWidth)
return () => window.removeEventListener('resize', clampToAvailableWidth)
}, [getMaxAllowedWidth])
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()
startXRef.current = e.clientX
startWidthRef.current = width
setIsResizing(true)
const handleMouseMove = (event: MouseEvent) => {
// Pane sits on the right: dragging left grows it.
const delta = startXRef.current - event.clientX
const maxAllowedWidth = getMaxAllowedWidth()
setWidth(clampPaneWidth(startWidthRef.current + delta, maxAllowedWidth))
}
const handleMouseUp = () => {
setIsResizing(false)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}, [width, getMaxAllowedWidth])
return (
<div
ref={paneRef}
onMouseDownCapture={onActivate}
className={cn(
'relative flex min-h-0 min-w-0 shrink-0 flex-col overflow-hidden border-l border-border bg-background',
className,
)}
style={{ width, flex: '0 0 auto' }}
>
<div
onMouseDown={handleMouseDown}
className={cn(
'absolute inset-y-0 left-0 z-20 w-4 -translate-x-1/2 cursor-col-resize',
'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] after:transition-colors',
'hover:after:bg-sidebar-border',
isResizing && 'after:bg-primary',
)}
/>
{children}
</div>
)
}

View file

@ -76,10 +76,21 @@ export function SessionRail({
return (
<div key={project.id} className="mb-3">
<div className="group flex items-center gap-1.5 px-1 py-1">
<FolderGit2 className="size-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate text-xs font-medium" title={project.path}>
{project.name}
</span>
{/* Deliberate hover delay the full path is reference info,
not something that should pop up on a passing cursor. */}
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<span className="flex min-w-0 flex-1 items-center gap-1.5">
<FolderGit2 className="size-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate text-xs font-medium">
{project.name}
</span>
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-[420px] break-all font-mono text-xs">
{project.path}
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="sm"