mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
fixes
This commit is contained in:
parent
3384f0f38f
commit
52772dd8dd
4 changed files with 158 additions and 10 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue