feat: better composer etc
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user