mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(desktop): move composer out of contain wrapper instead of portaling
Replaces the body-portal approach: render ChatBar as a sibling of the contain:[layout paint] chat wrapper (inside the same runtime boundary) rather than portaling the floating instance to <body>. The wrapper is a containing block for — and clips — position:fixed descendants, which is what stranded the popped-out composer off-screen. As a sibling it anchors to the outer relative container: docked stays absolute (identical placement), floating resolves against the viewport. Both states stay mounted, so dock<->float no longer remounts the editor (the portal toggle did).
This commit is contained in:
parent
79f270f549
commit
aff5ae692f
2 changed files with 65 additions and 71 deletions
|
|
@ -12,7 +12,6 @@ import {
|
|||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import { hermesDirectiveFormatter, type SlashChipKind } from '@/components/assistant-ui/directive-text'
|
||||
import { composerFill, composerSurfaceGlass } from '@/components/chat/composer-dock'
|
||||
|
|
@ -1924,7 +1923,7 @@ export function ChatBar({
|
|||
</div>
|
||||
)
|
||||
|
||||
const composerOverlay = (
|
||||
return (
|
||||
<>
|
||||
{dragging && poppedOut && (
|
||||
<div
|
||||
|
|
@ -2107,19 +2106,6 @@ export function ChatBar({
|
|||
</div>
|
||||
</ComposerPrimitive.Root>
|
||||
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating: portal to <body> so position:fixed resolves against the
|
||||
viewport. The chat content wrapper sets `contain: layout paint`, which
|
||||
makes it a containing block for (and clips) fixed descendants — left
|
||||
inline, the popped-out composer is positioned/clipped relative to the
|
||||
chat column (which shifts with the sidebars), not the viewport, so the
|
||||
viewport-based clamp can't keep it on-screen. Docked stays inline: it's
|
||||
`absolute` within that column by design. */}
|
||||
{poppedOut ? createPortal(composerOverlay, document.body) : composerOverlay}
|
||||
|
||||
<UrlDialog
|
||||
inputRef={urlInputRef}
|
||||
|
|
|
|||
|
|
@ -433,17 +433,17 @@ export function ChatView({
|
|||
|
||||
<PromptOverlays />
|
||||
|
||||
<div
|
||||
className="relative min-h-0 max-w-full flex-1 overflow-hidden bg-(--ui-chat-surface-background) contain-[layout_paint]"
|
||||
{...dropHandlers}
|
||||
<ChatRuntimeBoundary
|
||||
busy={busy}
|
||||
onCancel={onCancel}
|
||||
onEdit={onEdit}
|
||||
onReload={onReload}
|
||||
onThreadMessagesChange={onThreadMessagesChange}
|
||||
suppressMessages={routeSessionMismatch}
|
||||
>
|
||||
<ChatRuntimeBoundary
|
||||
busy={busy}
|
||||
onCancel={onCancel}
|
||||
onEdit={onEdit}
|
||||
onReload={onReload}
|
||||
onThreadMessagesChange={onThreadMessagesChange}
|
||||
suppressMessages={routeSessionMismatch}
|
||||
<div
|
||||
className="relative min-h-0 max-w-full flex-1 overflow-hidden bg-(--ui-chat-surface-background) contain-[layout_paint]"
|
||||
{...dropHandlers}
|
||||
>
|
||||
<Thread
|
||||
clampToComposer={showChatBar}
|
||||
|
|
@ -458,54 +458,62 @@ export function ChatView({
|
|||
sessionId={activeSessionId}
|
||||
sessionKey={threadKey}
|
||||
/>
|
||||
{showChatBar && (
|
||||
<Suspense fallback={<ChatBarFallback />}>
|
||||
<ChatBar
|
||||
busy={busy}
|
||||
cwd={currentCwd}
|
||||
disabled={!gatewayOpen}
|
||||
focusKey={activeSessionId}
|
||||
gateway={gateway}
|
||||
maxRecordingSeconds={maxVoiceRecordingSeconds}
|
||||
onAddContextRef={onAddContextRef}
|
||||
onAddUrl={onAddUrl}
|
||||
onAttachDroppedItems={onAttachDroppedItems}
|
||||
onAttachImageBlob={onAttachImageBlob}
|
||||
onCancel={onCancel}
|
||||
onPasteClipboardImage={onPasteClipboardImage}
|
||||
onPickFiles={onPickFiles}
|
||||
onPickFolders={onPickFolders}
|
||||
onPickImages={onPickImages}
|
||||
onRemoveAttachment={onRemoveAttachment}
|
||||
onSteer={onSteer}
|
||||
onSubmit={onSubmit}
|
||||
onTranscribeAudio={onTranscribeAudio}
|
||||
queueSessionKey={selectedSessionId}
|
||||
sessionId={activeSessionId}
|
||||
state={chatBarState}
|
||||
/>
|
||||
</Suspense>
|
||||
{resumeExhausted && routedSessionId && (
|
||||
<div className="absolute inset-0 z-10 grid place-items-center bg-(--ui-chat-surface-background) px-8 py-10">
|
||||
<ErrorState
|
||||
className="max-w-sm"
|
||||
description={t.desktop.resumeStrandedBody}
|
||||
title={t.desktop.resumeStrandedTitle}
|
||||
>
|
||||
<div className="grid justify-items-center">
|
||||
<Button onClick={() => onRetryResume(routedSessionId)} size="sm" variant="outline">
|
||||
{t.desktop.resumeRetry}
|
||||
</Button>
|
||||
</div>
|
||||
</ErrorState>
|
||||
</div>
|
||||
)}
|
||||
</ChatRuntimeBoundary>
|
||||
{resumeExhausted && routedSessionId && (
|
||||
<div className="absolute inset-0 z-10 grid place-items-center bg-(--ui-chat-surface-background) px-8 py-10">
|
||||
<ErrorState
|
||||
className="max-w-sm"
|
||||
description={t.desktop.resumeStrandedBody}
|
||||
title={t.desktop.resumeStrandedTitle}
|
||||
>
|
||||
<div className="grid justify-items-center">
|
||||
<Button onClick={() => onRetryResume(routedSessionId)} size="sm" variant="outline">
|
||||
{t.desktop.resumeRetry}
|
||||
</Button>
|
||||
</div>
|
||||
</ErrorState>
|
||||
</div>
|
||||
{showChatBar && <ScrollToBottomButton />}
|
||||
<ChatDropOverlay kind={dragKind} />
|
||||
<ChatSwapOverlay profile={gatewaySwapTarget} />
|
||||
</div>
|
||||
{/* Composer renders OUTSIDE the contain:[layout paint] wrapper above:
|
||||
that wrapper is a containing block for — and clips — position:fixed
|
||||
descendants, so the popped-out (fixed) composer would anchor to the
|
||||
chat column (which shifts/resizes with the sidebars) and get clipped
|
||||
off-screen instead of floating against the viewport. As a sibling it
|
||||
anchors to the outer relative container instead: docked is absolute
|
||||
(identical placement), floating resolves against the viewport. Both
|
||||
states stay mounted here, so dock⇄float never remounts the editor. */}
|
||||
{showChatBar && (
|
||||
<Suspense fallback={<ChatBarFallback />}>
|
||||
<ChatBar
|
||||
busy={busy}
|
||||
cwd={currentCwd}
|
||||
disabled={!gatewayOpen}
|
||||
focusKey={activeSessionId}
|
||||
gateway={gateway}
|
||||
maxRecordingSeconds={maxVoiceRecordingSeconds}
|
||||
onAddContextRef={onAddContextRef}
|
||||
onAddUrl={onAddUrl}
|
||||
onAttachDroppedItems={onAttachDroppedItems}
|
||||
onAttachImageBlob={onAttachImageBlob}
|
||||
onCancel={onCancel}
|
||||
onPasteClipboardImage={onPasteClipboardImage}
|
||||
onPickFiles={onPickFiles}
|
||||
onPickFolders={onPickFolders}
|
||||
onPickImages={onPickImages}
|
||||
onRemoveAttachment={onRemoveAttachment}
|
||||
onSteer={onSteer}
|
||||
onSubmit={onSubmit}
|
||||
onTranscribeAudio={onTranscribeAudio}
|
||||
queueSessionKey={selectedSessionId}
|
||||
sessionId={activeSessionId}
|
||||
state={chatBarState}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
{showChatBar && <ScrollToBottomButton />}
|
||||
<ChatDropOverlay kind={dragKind} />
|
||||
<ChatSwapOverlay profile={gatewaySwapTarget} />
|
||||
</div>
|
||||
</ChatRuntimeBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue