fix(dashboard): portal NewAgentDialog so it escapes the AgentsView stacking context

On mobile the dialog was rendering with its top hidden behind the in-page
\"Agents\" header. The dialog wasn't actually positioned wrong — its overlay
is `position: fixed; inset: 0`. The problem was that NewAgentDialog
rendered as a child of `.agents-view` (which has `overflow: hidden` and
`position: relative`), and an ancestor stacking context combined with the
relative+overflow parent prevented the fixed overlay from escaping above
the page header in the painter's order.

Render the dialog through `createPortal(..., document.body)` so the
overlay attaches at the document root, escaping every parent stacking
context. This is the standard React modal pattern.

All 9639 dashboard tests still pass — React Testing Library queries
traverse portals transparently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-28 00:23:45 -07:00
parent 0719fc0d1c
commit 4cb6ac4ef8

View File

@@ -1,5 +1,6 @@
import "./NewAgentDialog.css";
import { useState, useEffect, useCallback } from "react";
import { createPortal } from "react-dom";
import type { Agent, AgentCapability, ModelInfo, AgentGenerationSpec, PluginRuntimeInfo } from "../api";
import { createAgent, fetchAgents, fetchModels, updateGlobalSettings } from "../api";
import * as apiModule from "../api";
@@ -358,7 +359,13 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
</>
);
return (
// Render through a portal to document.body so the overlay escapes any
// ancestor stacking context / `overflow: hidden`. Without this, `position:
// fixed` on the overlay was being trapped under .agents-view, so the
// dialog rendered with its top hidden behind the in-page Agents header on
// mobile (the header isn't taller than the dialog top — it's just stacked
// above it because the dialog couldn't escape its container).
return createPortal(
<div className="agent-dialog-overlay" onClick={(e) => { if (e.target === e.currentTarget) handleClose(); }}>
<div className="agent-dialog" role="dialog" aria-modal="true" aria-label="Create new agent">
{/* Header */}
@@ -778,6 +785,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
onGenerated={handleGenerated}
projectId={projectId}
/>
</div>
</div>,
document.body,
);
}