feat: file tabs

This commit is contained in:
Brooklyn Nicholson
2026-05-05 13:17:40 -05:00
parent 5ec0667fb3
commit 5269012c51
27 changed files with 763 additions and 133 deletions
+7 -1
View File
@@ -38,7 +38,13 @@ import { useComposerGlassTweaks } from './hooks/use-composer-glass-tweaks'
import { useSlashCompletions } from './hooks/use-slash-completions'
import { useVoiceConversation } from './hooks/use-voice-conversation'
import { useVoiceRecorder } from './hooks/use-voice-recorder'
import { composerPlainText, placeCaretEnd, refChipElement, renderComposerContents, RICH_INPUT_SLOT } from './rich-editor'
import {
composerPlainText,
placeCaretEnd,
refChipElement,
renderComposerContents,
RICH_INPUT_SLOT
} from './rich-editor'
import { SkinSlashPopover } from './skin-slash-popover'
import { ComposerTriggerPopover } from './trigger-popover'
import type { ChatBarProps } from './types'
@@ -36,6 +36,7 @@ type PreviewWebview = HTMLElement & {
}
interface PreviewPaneProps {
embedded?: boolean
onClose: () => void
onRestartServer?: (url: string, context?: string) => Promise<string>
reloadRequest?: number
@@ -359,15 +360,30 @@ function PreviewConsolePanel({
const selectedLogIds = useStore(consoleState.$selectedLogIds)
const visibleSelection = useMemo(() => logs.filter(log => selectedLogIds.has(log.id)), [logs, selectedLogIds])
const sendableLogs = visibleSelection.length > 0 ? visibleSelection : logs
const stickScrollRafRef = useRef<number | null>(null)
useEffect(() => {
if (!consoleShouldStickRef.current) {
return
}
const consoleBody = consoleBodyRef.current
if (stickScrollRafRef.current !== null) {
window.cancelAnimationFrame(stickScrollRafRef.current)
stickScrollRafRef.current = null
}
consoleBody?.scrollTo({ top: consoleBody.scrollHeight })
stickScrollRafRef.current = window.requestAnimationFrame(() => {
stickScrollRafRef.current = null
const consoleBody = consoleBodyRef.current
consoleBody?.scrollTo({ top: consoleBody.scrollHeight })
})
return () => {
if (stickScrollRafRef.current !== null) {
window.cancelAnimationFrame(stickScrollRafRef.current)
stickScrollRafRef.current = null
}
}
}, [consoleBodyRef, consoleHeight, consoleShouldStickRef, logs])
function sendLogsToComposer(entries: ConsoleEntry[]) {
@@ -917,6 +933,7 @@ function LocalFilePreview({ reloadKey, target }: { reloadKey: number; target: Pr
const TITLEBAR_GROUP_ID = 'preview'
export function PreviewPane({
embedded = false,
onClose,
onRestartServer,
reloadRequest = 0,
@@ -1136,9 +1153,12 @@ export function PreviewPane({
consoleShouldStickRef.current = true
const consoleBody = consoleBodyRef.current
const handle = window.requestAnimationFrame(() => {
const consoleBody = consoleBodyRef.current
consoleBody?.scrollTo({ top: consoleBody.scrollHeight })
})
consoleBody?.scrollTo({ top: consoleBody.scrollHeight })
return () => window.cancelAnimationFrame(handle)
}, [consoleOpen])
useEffect(() => {
@@ -1423,21 +1443,23 @@ export function PreviewPane({
}, [appendConsoleEntry, consoleState, isWebPreview, target.url])
return (
<aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-border/60 bg-background text-muted-foreground">
<aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden bg-background text-muted-foreground">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="pointer-events-none flex min-h-(--titlebar-height) items-center gap-1.5 border-b border-border/60 bg-background px-2 py-1">
<div className="min-w-0 flex-1">
<a
className="pointer-events-auto inline max-w-full cursor-pointer truncate text-left text-xs font-medium text-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
href={currentUrl}
rel="noreferrer"
target="_blank"
title={`Open ${currentUrl}`}
>
{previewLabel || 'Preview'}
</a>
{!embedded && (
<div className="pointer-events-none flex min-h-(--titlebar-height) items-center gap-1.5 border-b border-border/60 bg-background px-2 py-1">
<div className="min-w-0 flex-1">
<a
className="pointer-events-auto inline max-w-full cursor-pointer truncate text-left text-xs font-medium text-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
href={currentUrl}
rel="noreferrer"
target="_blank"
title={`Open ${currentUrl}`}
>
{previewLabel || 'Preview'}
</a>
</div>
</div>
</div>
)}
<div
className="pointer-events-auto relative min-h-0 flex-1 overflow-hidden bg-background"
+153 -13
View File
@@ -1,12 +1,23 @@
import { useStore } from '@nanostores/react'
import { type MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react'
import type { SetTitlebarToolGroup } from '@/app/shell/titlebar-controls'
import { X } from '@/lib/icons'
import { cn } from '@/lib/utils'
import {
$filePreviewTarget,
$rightRailActiveTabId,
RIGHT_RAIL_PREVIEW_TAB_ID,
type RightRailTabId,
selectRightRailTab
} from '@/store/layout'
import {
$filePreviewTabs,
$previewReloadRequest,
$previewTarget,
dismissFilePreviewTarget,
dismissPreviewTarget
closeFilePreviewTab,
dismissPreviewTarget,
type FilePreviewTab,
type PreviewTarget
} from '@/store/preview'
import { PreviewPane } from './preview-pane'
@@ -27,23 +38,152 @@ interface ChatPreviewRailProps {
setTitlebarToolGroup?: SetTitlebarToolGroup
}
interface RailTab {
closeLabel: string
id: RightRailTabId
label: string
target: PreviewTarget
}
function previewTabLabel(target: PreviewTarget): string {
const value = target.label || target.path || target.source || target.url
const parts = value.split(/[\\/]/).filter(Boolean)
return parts.at(-1) || value || 'Preview'
}
function tabLabel(tab: FilePreviewTab): string {
return previewTabLabel(tab.target)
}
export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatPreviewRailProps) {
const previewReloadRequest = useStore($previewReloadRequest)
const filePreviewTarget = useStore($filePreviewTarget)
const activeTabId = useStore($rightRailActiveTabId)
const filePreviewTabs = useStore($filePreviewTabs)
const previewTarget = useStore($previewTarget)
const target = filePreviewTarget ?? previewTarget
if (!target) {
const tabs = useMemo<readonly RailTab[]>(
() => [
...(previewTarget
? [
{
closeLabel: 'Close preview',
id: RIGHT_RAIL_PREVIEW_TAB_ID,
label: 'Preview',
target: previewTarget
} satisfies RailTab
]
: []),
...filePreviewTabs.map(tab => ({
closeLabel: `Close ${tabLabel(tab)}`,
id: tab.id,
label: tabLabel(tab),
target: tab.target
}))
],
[filePreviewTabs, previewTarget]
)
const activeTab = tabs.find(tab => tab.id === activeTabId) ?? tabs[0]
// Read-by-ref so close handlers stay reference-stable across renders.
const activeTabRef = useRef<RailTab | undefined>(activeTab)
activeTabRef.current = activeTab
useEffect(() => {
if (activeTab && activeTab.id !== activeTabId) {
selectRightRailTab(activeTab.id)
}
}, [activeTab, activeTabId])
const closeRailTab = useCallback((tab: RailTab) => {
if (tab.id === RIGHT_RAIL_PREVIEW_TAB_ID) {
dismissPreviewTarget()
return
}
closeFilePreviewTab(tab.id)
}, [])
// Stable: PreviewPane lists onClose in a useEffect dep array that pushes
// titlebar tools. A fresh closure every render → setTitlebarToolGroup every
// render → DesktopController setState → re-render → ∞.
const handleCloseDocument = useCallback(() => {
const tab = activeTabRef.current
if (tab) {
closeRailTab(tab)
}
}, [closeRailTab])
const closeTab = (event: MouseEvent, tab: RailTab) => {
event.stopPropagation()
closeRailTab(tab)
}
if (!activeTab) {
return null
}
const isPreview = activeTab.id === RIGHT_RAIL_PREVIEW_TAB_ID
return (
<PreviewPane
onClose={filePreviewTarget ? dismissFilePreviewTarget : dismissPreviewTarget}
onRestartServer={filePreviewTarget ? undefined : onRestartServer}
reloadRequest={previewReloadRequest}
setTitlebarToolGroup={setTitlebarToolGroup}
target={target}
/>
<aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-border/60 bg-background text-muted-foreground">
<div
className="flex h-(--titlebar-height) shrink-0 overflow-x-auto overflow-y-hidden overscroll-x-contain border-b border-border/60 bg-[color-mix(in_srgb,var(--dt-sidebar-bg)_94%,transparent)] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
role="tablist"
>
{tabs.map(tab => {
const active = tab.id === activeTab.id
return (
<div
className={cn(
'group/tab relative flex h-full max-w-48 shrink-0 items-center text-[0.6875rem] font-medium [-webkit-app-region:no-drag]',
active
? 'bg-background text-foreground'
: 'border-r border-border/40 text-muted-foreground hover:bg-accent/30 hover:text-foreground'
)}
key={tab.id}
>
{active && <span aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-primary/70" />}
<button
aria-selected={active}
className="flex h-full min-w-0 flex-1 items-center truncate pl-3 pr-1.5 text-left outline-none"
onClick={() => selectRightRailTab(tab.id)}
role="tab"
title={tab.label}
type="button"
>
{tab.label}
</button>
<button
aria-label={tab.closeLabel}
className={cn(
'mr-1.5 hidden size-4 shrink-0 place-items-center rounded-sm text-muted-foreground/55 transition-colors hover:bg-accent hover:text-foreground focus-visible:grid group-hover/tab:grid',
active && 'grid'
)}
onClick={event => closeTab(event, tab)}
title={tab.closeLabel}
type="button"
>
<X className="size-3" />
</button>
</div>
)
})}
</div>
<div className="min-h-0 flex-1 overflow-hidden">
<PreviewPane
embedded
onClose={handleCloseDocument}
onRestartServer={isPreview ? onRestartServer : undefined}
reloadRequest={previewReloadRequest}
setTitlebarToolGroup={setTitlebarToolGroup}
target={activeTab.target}
/>
</div>
</aside>
)
}
+53 -35
View File
@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react'
import { useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useRef } from 'react'
import { lazy, Suspense, useCallback, useEffect, useRef } from 'react'
import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom'
import { Pane, PaneMain } from '@/components/pane-shell'
@@ -35,13 +35,15 @@ import {
setSessionsLoading
} from '../store/session'
import { AgentsView } from './agents'
import { ArtifactsView } from './artifacts'
import { ChatView } from './chat'
import { useComposerActions } from './chat/hooks/use-composer-actions'
import { ChatPreviewRail, PREVIEW_RAIL_MAX_WIDTH, PREVIEW_RAIL_MIN_WIDTH, PREVIEW_RAIL_PANE_WIDTH } from './chat/right-rail'
import {
ChatPreviewRail,
PREVIEW_RAIL_MAX_WIDTH,
PREVIEW_RAIL_MIN_WIDTH,
PREVIEW_RAIL_PANE_WIDTH
} from './chat/right-rail'
import { ChatSidebar } from './chat/sidebar'
import { CommandCenterView } from './command-center'
import { FileBrowserPane } from './file-browser'
import { useGatewayBoot } from './gateway/hooks/use-gateway-boot'
import { useGatewayRequest } from './gateway/hooks/use-gateway-request'
@@ -57,7 +59,6 @@ import { usePromptActions } from './session/hooks/use-prompt-actions'
import { useRouteResume } from './session/hooks/use-route-resume'
import { useSessionActions } from './session/hooks/use-session-actions'
import { useSessionStateCache } from './session/hooks/use-session-state-cache'
import { SettingsView } from './settings'
import { AppShell } from './shell/app-shell'
import { useOverlayRouting } from './shell/hooks/use-overlay-routing'
import { useStatusSnapshot } from './shell/hooks/use-status-snapshot'
@@ -65,7 +66,12 @@ import { useStatusbarItems } from './shell/hooks/use-statusbar-items'
import type { StatusbarItem } from './shell/statusbar-controls'
import type { TitlebarTool } from './shell/titlebar-controls'
import { useGroupRegistry } from './shell/use-group-registry'
import { SkillsView } from './skills'
const AgentsView = lazy(async () => ({ default: (await import('./agents')).AgentsView }))
const ArtifactsView = lazy(async () => ({ default: (await import('./artifacts')).ArtifactsView }))
const CommandCenterView = lazy(async () => ({ default: (await import('./command-center')).CommandCenterView }))
const SettingsView = lazy(async () => ({ default: (await import('./settings')).SettingsView }))
const SkillsView = lazy(async () => ({ default: (await import('./skills')).SkillsView }))
export function DesktopController() {
const queryClient = useQueryClient()
@@ -404,34 +410,42 @@ export function DesktopController() {
<ModelPickerOverlay gateway={gatewayRef.current || undefined} onSelect={selectModel} />
{settingsOpen && (
<SettingsView
onClose={closeOverlayToPreviousRoute}
onConfigSaved={() => {
void refreshHermesConfig()
void refreshCurrentModel()
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
}}
/>
<Suspense fallback={null}>
<SettingsView
onClose={closeOverlayToPreviousRoute}
onConfigSaved={() => {
void refreshHermesConfig()
void refreshCurrentModel()
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
}}
/>
</Suspense>
)}
{commandCenterOpen && (
<CommandCenterView
initialSection={commandCenterInitialSection}
onClose={closeOverlayToPreviousRoute}
onDeleteSession={removeSession}
onMainModelChanged={(provider, model) => {
setCurrentProvider(provider)
setCurrentModel(model)
updateModelOptionsCache(provider, model, true)
void refreshCurrentModel()
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
}}
onNavigateRoute={path => navigate(path)}
onOpenSession={sessionId => navigate(sessionRoute(sessionId))}
/>
<Suspense fallback={null}>
<CommandCenterView
initialSection={commandCenterInitialSection}
onClose={closeOverlayToPreviousRoute}
onDeleteSession={removeSession}
onMainModelChanged={(provider, model) => {
setCurrentProvider(provider)
setCurrentModel(model)
updateModelOptionsCache(provider, model, true)
void refreshCurrentModel()
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
}}
onNavigateRoute={path => navigate(path)}
onOpenSession={sessionId => navigate(sessionRoute(sessionId))}
/>
</Suspense>
)}
{agentsOpen && <AgentsView onClose={closeOverlayToPreviousRoute} />}
{agentsOpen && (
<Suspense fallback={null}>
<AgentsView onClose={closeOverlayToPreviousRoute} />
</Suspense>
)}
</>
)
@@ -489,16 +503,20 @@ export function DesktopController() {
<Route element={chatView} path=":sessionId" />
<Route
element={
<SkillsView setStatusbarItemGroup={setStatusbarItemGroup} setTitlebarToolGroup={setTitlebarToolGroup} />
<Suspense fallback={null}>
<SkillsView setStatusbarItemGroup={setStatusbarItemGroup} setTitlebarToolGroup={setTitlebarToolGroup} />
</Suspense>
}
path="skills"
/>
<Route
element={
<ArtifactsView
setStatusbarItemGroup={setStatusbarItemGroup}
setTitlebarToolGroup={setTitlebarToolGroup}
/>
<Suspense fallback={null}>
<ArtifactsView
setStatusbarItemGroup={setStatusbarItemGroup}
setTitlebarToolGroup={setTitlebarToolGroup}
/>
</Suspense>
}
path="artifacts"
/>
@@ -24,12 +24,14 @@ interface FileBrowserPaneProps {
export function FileBrowserPane({ onActivateFile, onChangeCwd }: FileBrowserPaneProps) {
const currentCwd = useStore($currentCwd).trim()
const hasCwd = currentCwd.length > 0
const cwdName = hasCwd
? (currentCwd
.split(/[\\/]+/)
.filter(Boolean)
.pop() ?? currentCwd)
: 'No folder selected'
const { data, loadChildren, openState, refreshRoot, rootError, rootLoading, setNodeOpen } = useProjectTree(currentCwd)
const chooseFolder = async () => {
@@ -179,6 +179,7 @@ export function useProjectTree(cwd: string): UseProjectTreeResult {
if (!cwd || inflight.has(id)) {
return
}
inflight.add(id)
setProjectTree(current => {
@@ -1,5 +1,5 @@
import type { QueryClient } from '@tanstack/react-query'
import { type MutableRefObject, useCallback } from 'react'
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
import {
appendAssistantTextPart,
@@ -51,6 +51,13 @@ interface MessageStreamOptions {
) => ClientSessionState
}
interface QueuedStreamDeltas {
assistant: string
reasoning: string
}
const STREAM_DELTA_FLUSH_MS = 16
export function useMessageStream({
activeSessionIdRef,
hydrateFromStoredSession,
@@ -123,19 +130,108 @@ export function useMessageStream({
[updateSessionState]
)
const queuedDeltasRef = useRef<Map<string, QueuedStreamDeltas>>(new Map())
const flushHandleRef = useRef<number | null>(null)
const flushQueuedDeltas = useCallback(
(sessionId?: string) => {
const queue = queuedDeltasRef.current
const ids = sessionId ? [sessionId] : [...queue.keys()]
for (const id of ids) {
const queued = queue.get(id)
if (!queued) {
continue
}
queue.delete(id)
if (queued.assistant) {
mutateStream(
id,
parts => appendAssistantTextPart(parts, queued.assistant),
() => [assistantTextPart(queued.assistant)]
)
}
if (queued.reasoning) {
mutateStream(
id,
parts => appendReasoningPart(parts, queued.reasoning),
() => [reasoningPart(queued.reasoning)]
)
}
}
},
[mutateStream]
)
const scheduleDeltaFlush = useCallback(() => {
if (flushHandleRef.current !== null) {
return
}
if (typeof window === 'undefined') {
flushQueuedDeltas()
return
}
if (typeof window.requestAnimationFrame === 'function') {
flushHandleRef.current = window.requestAnimationFrame(() => {
flushHandleRef.current = null
flushQueuedDeltas()
})
return
}
flushHandleRef.current = window.setTimeout(() => {
flushHandleRef.current = null
flushQueuedDeltas()
}, STREAM_DELTA_FLUSH_MS)
}, [flushQueuedDeltas])
const queueDelta = useCallback(
(sessionId: string, key: keyof QueuedStreamDeltas, delta: string) => {
if (!delta) {
return
}
const queued = queuedDeltasRef.current.get(sessionId) ?? { assistant: '', reasoning: '' }
queued[key] += delta
queuedDeltasRef.current.set(sessionId, queued)
scheduleDeltaFlush()
},
[scheduleDeltaFlush]
)
useEffect(
() => () => {
if (flushHandleRef.current !== null && typeof window !== 'undefined') {
if (typeof window.cancelAnimationFrame === 'function') {
window.cancelAnimationFrame(flushHandleRef.current)
} else {
window.clearTimeout(flushHandleRef.current)
}
}
flushHandleRef.current = null
flushQueuedDeltas()
},
[flushQueuedDeltas]
)
const appendAssistantDelta = useCallback(
(sessionId: string, delta: string) => {
if (!delta) {
return
}
mutateStream(
sessionId,
parts => appendAssistantTextPart(parts, delta),
() => [assistantTextPart(delta)]
)
queueDelta(sessionId, 'assistant', delta)
},
[mutateStream]
[queueDelta]
)
const appendReasoningDelta = useCallback(
@@ -144,6 +240,14 @@ export function useMessageStream({
return
}
if (!replace) {
queueDelta(sessionId, 'reasoning', delta)
return
}
flushQueuedDeltas(sessionId)
mutateStream(
sessionId,
(parts, message) => {
@@ -160,7 +264,7 @@ export function useMessageStream({
() => [reasoningPart(delta)]
)
},
[mutateStream]
[flushQueuedDeltas, mutateStream, queueDelta]
)
const upsertToolCall = useCallback(
@@ -384,6 +488,8 @@ export function useMessageStream({
return
}
flushQueuedDeltas(sessionId)
if (isActiveEvent) {
triggerHaptic('streamStart')
}
@@ -421,6 +527,8 @@ export function useMessageStream({
return
}
flushQueuedDeltas(sessionId)
if (isActiveEvent) {
triggerHaptic('streamDone')
}
@@ -440,9 +548,12 @@ export function useMessageStream({
return
}
flushQueuedDeltas(sessionId)
upsertToolCall(sessionId, payload, 'running')
} else if (event.type === 'tool.complete') {
if (sessionId) {
flushQueuedDeltas(sessionId)
upsertToolCall(sessionId, payload, 'complete')
}
@@ -478,6 +589,7 @@ export function useMessageStream({
}
if (sessionId) {
flushQueuedDeltas(sessionId)
updateSessionState(sessionId, state => ({
...state,
awaitingResponse: false,
@@ -495,6 +607,7 @@ export function useMessageStream({
appendReasoningDelta,
activeSessionIdRef,
completeAssistantMessage,
flushQueuedDeltas,
queryClient,
refreshHermesConfig,
updateSessionState,
@@ -65,6 +65,7 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway
if (selection.persistGlobal) {
void refreshCurrentModel()
}
void queryClient.invalidateQueries({
queryKey: selection.persistGlobal ? ['model-options'] : ['model-options', activeSessionId]
})
@@ -134,11 +134,13 @@ export function usePreviewRouting({
if (!candidate) {
return
}
const desktop = window.hermesDesktop
if (!desktop?.normalizePreviewTarget) {
return
}
const sessionId = previewSessionId
const cwd = currentCwd || ''
const target = await desktop.normalizePreviewTarget(candidate, cwd || undefined).catch(() => null)
@@ -25,6 +25,7 @@ function rawHashLooksLikeSession(): boolean {
if (typeof window === 'undefined') {
return false
}
const hash = window.location.hash.replace(/^#/, '')
if (!hash || hash === '/') {
@@ -29,6 +29,8 @@ export function useSessionStateCache({
const selectedStoredSessionIdRef = useRef<string | null>(null)
const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
const runtimeIdByStoredSessionIdRef = useRef(new Map<string, string>())
const pendingViewStateRef = useRef<{ sessionId: string; state: ClientSessionState } | null>(null)
const viewSyncRafRef = useRef<number | null>(null)
useEffect(() => {
activeSessionIdRef.current = activeSessionId
@@ -78,18 +80,60 @@ export function useSessionStateCache({
const syncSessionStateToView = useCallback(
(sessionId: string, state: ClientSessionState) => {
if (sessionId !== activeSessionIdRef.current) {
pendingViewStateRef.current = { sessionId, state }
if (viewSyncRafRef.current !== null) {
return
}
setMessages(state.messages)
setBusy(state.busy)
busyRef.current = state.busy
setAwaitingResponse(state.awaitingResponse)
if (typeof window === 'undefined') {
const pending = pendingViewStateRef.current
if (!pending || pending.sessionId !== activeSessionIdRef.current) {
pendingViewStateRef.current = null
return
}
pendingViewStateRef.current = null
setMessages(pending.state.messages)
setBusy(pending.state.busy)
busyRef.current = pending.state.busy
setAwaitingResponse(pending.state.awaitingResponse)
return
}
viewSyncRafRef.current = window.requestAnimationFrame(() => {
viewSyncRafRef.current = null
const pending = pendingViewStateRef.current
if (!pending || pending.sessionId !== activeSessionIdRef.current) {
pendingViewStateRef.current = null
return
}
pendingViewStateRef.current = null
setMessages(pending.state.messages)
setBusy(pending.state.busy)
busyRef.current = pending.state.busy
setAwaitingResponse(pending.state.awaitingResponse)
})
},
[busyRef, setAwaitingResponse, setBusy, setMessages]
)
useEffect(
() => () => {
if (viewSyncRafRef.current !== null && typeof window !== 'undefined') {
window.cancelAnimationFrame(viewSyncRafRef.current)
viewSyncRafRef.current = null
}
},
[]
)
const updateSessionState = useCallback(
(
sessionId: string,
@@ -121,7 +121,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
{visiblePaneTools.length > 0 && (
<div
aria-label="Pane controls"
className="fixed top-(--titlebar-controls-top) right-[calc(var(--titlebar-tools-right)+var(--shell-preview-toolbar-gap,0px))] z-70 flex flex-row items-center gap-px pointer-events-auto select-none [-webkit-app-region:no-drag]"
className="fixed top-(--titlebar-controls-top) right-[calc(var(--titlebar-tools-right)+var(--shell-preview-toolbar-gap,0))] z-70 flex flex-row items-center gap-px pointer-events-auto select-none [-webkit-app-region:no-drag]"
>
{visiblePaneTools.map(tool => (
<TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />