feat: better composer etc

This commit is contained in:
Brooklyn Nicholson
2026-05-04 22:19:16 -05:00
parent 42db075e10
commit fcce49db3f
50 changed files with 1299 additions and 668 deletions
+9 -54
View File
@@ -1,19 +1,16 @@
import { useStore } from '@nanostores/react'
import type { CSSProperties, ReactNode, PointerEvent as ReactPointerEvent } from 'react'
import { useCallback } from 'react'
import type { CSSProperties, ReactNode } from 'react'
import { PaneShell } from '@/components/pane-shell'
import { SidebarProvider } from '@/components/ui/sidebar'
import { triggerHaptic } from '@/lib/haptics'
import {
$fileBrowserOpen,
$sidebarOpen,
$sidebarWidth,
FILE_BROWSER_DEFAULT_WIDTH,
setSidebarOpen,
setSidebarResizing,
setSidebarWidth
FILE_BROWSER_PANE_ID,
setSidebarOpen
} from '@/store/layout'
import { $paneWidthOverride } from '@/store/panes'
import { $connection } from '@/store/session'
import { StatusbarControls, type StatusbarItem } from './statusbar-controls'
@@ -39,9 +36,9 @@ export function AppShell({
statusbarItems,
titlebarTools
}: AppShellProps) {
const sidebarWidth = useStore($sidebarWidth)
const sidebarOpen = useStore($sidebarOpen)
const fileBrowserOpen = useStore($fileBrowserOpen)
const fileBrowserWidthOverride = useStore($paneWidthOverride(FILE_BROWSER_PANE_ID))
const connection = useStore($connection)
const titlebarControls = titlebarControlsPosition(connection?.windowButtonPosition)
@@ -57,12 +54,15 @@ export function AppShell({
const paneToolCount = titlebarTools?.filter(tool => !tool.hidden).length ?? 0
const systemToolsWidth = `calc(${SYSTEM_TOOL_COUNT} * var(--titlebar-control-size))`
const fileBrowserWidth =
fileBrowserWidthOverride !== undefined ? `${fileBrowserWidthOverride}px` : FILE_BROWSER_DEFAULT_WIDTH
// Where the pane-tool cluster's right edge sits, measured from the inner
// titlebar padding (--titlebar-tools-right). Two anchors:
// - file-browser closed → flush against static cluster's left edge
// - file-browser open → flush against the file-browser pane's left edge
// (= preview pane's right edge)
const previewToolbarGap = fileBrowserOpen ? FILE_BROWSER_DEFAULT_WIDTH : systemToolsWidth
const previewToolbarGap = fileBrowserOpen ? fileBrowserWidth : systemToolsWidth
// Used by the drag region to know where the rightmost interactive element
// ends. When pane tools are present, that's `gap + paneCount * controlSize`
@@ -73,38 +73,6 @@ export function AppShell({
? `calc(${previewToolbarGap} + ${paneToolCount} * var(--titlebar-control-size))`
: systemToolsWidth
const startSidebarResize = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
event.preventDefault()
setSidebarResizing(true)
const startX = event.clientX
const startWidth = sidebarWidth
const previousCursor = document.body.style.cursor
const previousUserSelect = document.body.style.userSelect
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
const handleMove = (moveEvent: PointerEvent) => {
setSidebarWidth(startWidth + moveEvent.clientX - startX)
}
const handleUp = () => {
setSidebarResizing(false)
triggerHaptic('crisp')
document.body.style.cursor = previousCursor
document.body.style.userSelect = previousUserSelect
window.removeEventListener('pointermove', handleMove)
window.removeEventListener('pointerup', handleUp)
}
window.addEventListener('pointermove', handleMove)
window.addEventListener('pointerup', handleUp, { once: true })
},
[sidebarWidth]
)
return (
<SidebarProvider
className="h-screen min-h-0 bg-background"
@@ -143,19 +111,6 @@ export function AppShell({
/>
{children}
{sidebarOpen && (
<div
aria-label="Resize sidebar"
aria-orientation="vertical"
className="group absolute bottom-0 top-0 left-[calc(var(--pane-chat-sidebar-width)-0.5rem)] z-5 w-4 cursor-col-resize [-webkit-app-region:no-drag]"
onPointerDown={startSidebarResize}
role="separator"
tabIndex={0}
>
<span className="absolute left-1/2 top-1/2 h-23 w-0.75 -translate-x-1/2 -translate-y-1/2 rounded-full bg-muted-foreground/80 opacity-0 transition-opacity duration-100 group-hover:opacity-[0.65] group-focus-visible:opacity-[0.65]" />
</div>
)}
</PaneShell>
<StatusbarControls items={statusbarItems} leftItems={leftStatusbarItems} />
@@ -20,7 +20,9 @@ export function useStatusSnapshot(gatewayState: string | undefined) {
getLogs({ file: 'gateway', lines: LOG_TAIL }).catch(() => ({ lines: [] }))
])
if (cancelled) {return}
if (cancelled) {
return
}
setStatusSnapshot(next)
setGatewayLogLines(logs.lines.map(line => line.trim()).filter(Boolean))
@@ -201,7 +201,18 @@ export function useStatusbarItems({
variant: 'text'
}
],
[browseSessionCwd, busy, contextBar, contextUsage, currentBranch, currentCwd, currentModel, currentProvider, sessionStartedAt, turnStartedAt]
[
browseSessionCwd,
busy,
contextBar,
contextUsage,
currentBranch,
currentCwd,
currentModel,
currentProvider,
sessionStartedAt,
turnStartedAt
]
)
const leftStatusbarItems = useMemo(
@@ -1,12 +1,7 @@
import type { ComponentProps, ReactNode } from 'react'
import { useNavigate } from 'react-router-dom'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
export interface StatusbarMenuItem {
@@ -62,26 +57,24 @@ export function StatusbarControls({ className, leftItems = [], items = [], ...pr
{...props}
>
<div className="flex min-w-0 items-center gap-0.5 overflow-x-auto">
{leftItems.filter(item => !item.hidden).map(item => (
<StatusbarItemView item={item} key={`left:${item.id}`} navigate={navigate} />
))}
{leftItems
.filter(item => !item.hidden)
.map(item => (
<StatusbarItemView item={item} key={`left:${item.id}`} navigate={navigate} />
))}
</div>
<div className="flex min-w-0 items-center gap-0.5 overflow-x-auto">
{items.filter(item => !item.hidden).map(item => (
<StatusbarItemView item={item} key={`right:${item.id}`} navigate={navigate} />
))}
{items
.filter(item => !item.hidden)
.map(item => (
<StatusbarItemView item={item} key={`right:${item.id}`} navigate={navigate} />
))}
</div>
</footer>
)
}
function StatusbarItemView({
item,
navigate
}: {
item: StatusbarItem
navigate: ReturnType<typeof useNavigate>
}) {
function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate: ReturnType<typeof useNavigate> }) {
const content = (
<>
{item.icon}
@@ -147,7 +140,12 @@ function StatusbarItemView({
if (item.variant === 'text' && !item.onSelect && !item.to && !item.href) {
return (
<div className={cn('inline-flex h-5 items-center gap-1 px-0.5 text-[0.68rem] text-muted-foreground/90', item.className)}>
<div
className={cn(
'inline-flex h-5 items-center gap-1 px-0.5 text-[0.68rem] text-muted-foreground/90',
item.className
)}
>
{content}
</div>
)