Skip to content

Commit 2d91a3c

Browse files
committed
fix(frontend): make the playground panels meet the divider and shrink cleanly
The config and chat panels were separated by a 12px dark channel, and neither header reached it. The config panel's sticky headers also stopped short of the panel edge once the scrollbar gutter was reserved. - The Build/Chat divider is a hairline in the shell's frame colour, so both panels butt against it. The grab zone stays 11px wide and the grip only appears on hover or drag. - Sticky headers stretch across the reserved scrollbar gutter, so they span the panel edge to edge. The gutter width is measured, since it is zero on overlay-scrollbar systems. - The chat panel gets the same quiet scrollbar as the config panel. - Below 320px the config panel drops what it can spare: Configuration becomes Config, Deploy and Commit become icon buttons, section rows keep their icon and summary, and list rows keep their name. It can now be dragged down to 240px.
1 parent 91b93af commit 2d91a3c

6 files changed

Lines changed: 142 additions & 43 deletions

File tree

web/oss/src/components/Playground/Components/MainLayout/index.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {chatPanelMaximizedAtom} from "@/oss/components/AgentChatSlice/state/pane
2222
// Direct file import — the SessionInspector barrel would statically pull the (dynamic,
2323
// open-on-demand) inspector drawer back into this chunk.
2424
import PanelSessionInspectorButton from "@/oss/components/SessionInspector/PanelSessionInspectorButton"
25+
import {useScrollbarGutterVar} from "@/oss/hooks/useScrollbarGutterVar"
2526
import {routerAppIdAtom} from "@/oss/state/app/selectors/app"
2627
import {playgroundEarlyAgentStateAtom} from "@/oss/state/workflow"
2728

@@ -268,9 +269,10 @@ const PlaygroundMainView = ({
268269
const animateSplit = justToggled || holdAnimate
269270

270271
const variantRefs = useRef<(HTMLDivElement | null)[]>([])
271-
const {setConfigPanelRef, setGenerationPanelRef} = usePlaygroundScrollSync({
272+
const {configPanelRef, setConfigPanelRef, setGenerationPanelRef} = usePlaygroundScrollSync({
272273
enabled: isComparisonView,
273274
})
275+
useScrollbarGutterVar(configPanelRef)
274276

275277
const handleAddRunnable = useCallback(async () => {
276278
const selection = await openEntitySelector({
@@ -365,7 +367,10 @@ const PlaygroundMainView = ({
365367
<SplitterPanel
366368
defaultSize={configDefaultSize}
367369
size={configCollapsed ? 0 : undefined}
368-
min="20%"
370+
// Agent config collapses to icon + summary rows below 320px (see the
371+
// `config` container query in globals.css), so it can go much narrower
372+
// than the percentage floor the prompt playground needs.
373+
min={isAgentConfig ? 240 : "20%"}
369374
max={configMaxSize}
370375
className="!h-full"
371376
collapsible={splitCollapsible}
@@ -375,16 +380,19 @@ const PlaygroundMainView = ({
375380
The notice lives OUTSIDE the scroller, so it sits at the pane's bottom
376381
edge regardless of content height or scroll position. */}
377382
<div
378-
className={clsx("flex h-full min-h-0 w-full flex-col", {
379-
// Config = the raised authoring surface (covers the notice too).
380-
"ag-panel-raised": isAgentConfig,
381-
})}
383+
className={clsx(
384+
"@container/config flex h-full min-h-0 w-full flex-col",
385+
{
386+
// Config = the raised authoring surface (covers the notice too).
387+
"ag-panel-raised": isAgentConfig,
388+
},
389+
)}
382390
>
383391
<section
384392
ref={setConfigPanelRef}
385393
className={clsx([
386394
{
387-
"ag-scroll-quiet grow w-full min-h-0 overflow-y-auto":
395+
"ag-scroll-quiet ag-scroll-bleed grow w-full min-h-0 overflow-y-auto":
388396
!isComparisonView,
389397
"grow w-full min-h-0 overflow-x-auto flex [&::-webkit-scrollbar]:w-0":
390398
isComparisonView,
@@ -457,7 +465,7 @@ const PlaygroundMainView = ({
457465
className={clsx([
458466
"playground-generation",
459467
{
460-
"grow w-full h-full overflow-y-auto overflow-x-hidden":
468+
"ag-scroll-quiet grow w-full h-full overflow-y-auto overflow-x-hidden":
461469
!isComparisonView,
462470
"grow w-full h-full overflow-auto [&::-webkit-scrollbar]:w-0":
463471
isComparisonView,

web/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,10 @@ const PlaygroundVariantConfigHeader = ({
203203
// agent name), so this bar reads as the config panel's "Configuration" header.
204204
// Also the neutral header while agent-ness is unknown, so the prompt chrome below
205205
// never flashes on load for an agent.
206+
// "Configuration" shortens to "Config" once the panel is too narrow for it.
206207
<span className="text-[13px] font-semibold text-[var(--ant-color-text)]">
207-
Configuration
208+
<span className="ag-config-title-long">Configuration</span>
209+
<span className="ag-config-title-short">Config</span>
208210
</span>
209211
) : (
210212
<>
@@ -267,7 +269,7 @@ const PlaygroundVariantConfigHeader = ({
267269
</>
268270
)}
269271
</div>
270-
<div className="flex items-center justify-end gap-2 shrink-0 grow min-w-0">
272+
<div className="ag-config-actions flex items-center justify-end gap-2 shrink-0 grow min-w-0">
271273
{extraActions}
272274
{hasPresets && onLoadPreset && (
273275
<Button size="small" onClick={onLoadPreset}>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {useEffect} from "react"
2+
3+
/**
4+
* Publishes a scroll container's reserved scrollbar gutter as `--ag-scroll-gutter` on the
5+
* element itself. The width is platform-dependent (0 on overlay-scrollbar systems), so it
6+
* has to be measured rather than assumed. Pairs with the `.ag-scroll-bleed` rule, which
7+
* lets full-width rows stretch across the gutter instead of stopping short of it.
8+
*/
9+
export const useScrollbarGutterVar = (element: HTMLElement | null) => {
10+
useEffect(() => {
11+
if (!element) return
12+
13+
const sync = () => {
14+
const gutter = element.offsetWidth - element.clientWidth
15+
element.style.setProperty("--ag-scroll-gutter", `${gutter}px`)
16+
}
17+
18+
sync()
19+
20+
const observer = new ResizeObserver(sync)
21+
observer.observe(element)
22+
23+
return () => observer.disconnect()
24+
}, [element])
25+
}
26+
27+
export default useScrollbarGutterVar

web/oss/src/styles/globals.css

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,38 @@ body {
155155
opacity: 0.4 !important;
156156
}
157157

158-
/* Agent playground: the collapse pill is disabled (Build/Chat switch lives in the header), so the
159-
drag handle is the ONLY resize affordance and must be discoverable on its own. Render a persistent
160-
centered grip on the divider — neutral at rest, then an accent TINT + taller on hover/drag — so it
161-
reads as a draggable handle without turning into a loud bar. Geometry is fully specified so it
162-
doesn't depend on antd's default pseudo-element placement. */
158+
/* Agent playground: the divider between Config and Chat is a hairline in the app's frame colour,
159+
so both panels butt straight against it and their headers read as one row. The bar itself stays
160+
1px; the dragger overhangs it to give a comfortable grab zone without widening the seam. */
161+
.playground-splitter-agent > .ant-splitter-bar {
162+
background: var(--ag-shell-line) !important;
163+
flex-basis: 1px !important;
164+
width: 1px !important;
165+
overflow: visible !important;
166+
}
167+
.playground-splitter-agent .ant-splitter-bar-dragger {
168+
position: absolute !important;
169+
left: 50% !important;
170+
right: auto !important;
171+
width: 11px !important;
172+
transform: translateX(-50%) !important;
173+
background: transparent !important;
174+
}
175+
/* Rest: the seam is just the bar. Hover and drag brighten it and float a short grip on it, so the
176+
resize affordance shows up exactly when the pointer is there and stays invisible otherwise. */
163177
.playground-splitter-agent .ant-splitter-bar-dragger::before {
164178
content: "" !important;
165179
position: absolute !important;
166180
top: 0 !important;
167181
bottom: 0 !important;
168182
left: 50% !important;
169183
right: auto !important;
170-
width: 2px !important;
184+
width: 1px !important;
171185
height: auto !important;
172186
border-radius: 0 !important;
173187
transform: translateX(-50%) !important;
174-
background: var(--ag-colorBorderSecondary) !important;
188+
background: transparent !important;
189+
transition: background 150ms ease;
175190
}
176191
.playground-splitter-agent .ant-splitter-bar-dragger::after {
177192
content: "" !important;
@@ -180,22 +195,16 @@ body {
180195
bottom: auto !important;
181196
left: 50% !important;
182197
right: auto !important;
183-
width: 4px !important;
184-
height: 24px !important;
198+
width: 3px !important;
199+
height: 28px !important;
185200
border-radius: 9999px !important;
186201
transform: translate(-50%, -50%) !important;
187202
background: var(--ag-colorTextTertiary) !important;
203+
opacity: 0 !important;
188204
transition:
189-
background 150ms ease,
190205
opacity 150ms ease,
191-
height 150ms ease;
192-
}
193-
/* Hover + active: the full-height DIVIDER only strengthens to a neutral border tone — a long line in
194-
the accent colour reads as too loud. The small GRIP takes the accent (`colorPrimary`), softened to a
195-
TINT via opacity so it stays a clear-but-quiet handle in both themes (the tinted-primary border
196-
token blended into the dark divider; the full colour was too strong). `:not(-active)` mirrors the
197-
base `.playground-splitter` hover rule's specificity so we win the tie by source order — otherwise
198-
that base rule would swap the divider to a faint fill on hover (the fade). */
206+
background 150ms ease;
207+
}
199208
.playground-splitter-agent
200209
.ant-splitter-bar-dragger:hover:not(.ant-splitter-bar-dragger-active)::before,
201210
.playground-splitter-agent .ant-splitter-bar-dragger-active::before {
@@ -204,18 +213,10 @@ body {
204213
.playground-splitter-agent
205214
.ant-splitter-bar-dragger:hover:not(.ant-splitter-bar-dragger-active)::after,
206215
.playground-splitter-agent .ant-splitter-bar-dragger-active::after {
207-
height: 36px !important;
208-
background: var(--ag-colorPrimary) !important;
209-
opacity: 0.7 !important;
216+
opacity: 1 !important;
210217
}
211-
/* Agent playground: the divider is the GUTTER between the raised Config panel and the recessed
212-
Chat canvas — a real ~12px channel (not a hairline) that says "two workspaces". Widen the bar and
213-
tint it the near-black/soft-grey gutter tone; the grip pseudo-elements above still read as the
214-
handle. */
215-
.playground-splitter-agent > .ant-splitter-bar {
216-
background: var(--ag-surface-gutter) !important;
217-
flex-basis: 12px !important;
218-
width: 12px !important;
218+
.playground-splitter-agent .ant-splitter-bar-dragger-active::after {
219+
background: var(--ag-colorPrimary) !important;
219220
}
220221

221222
/* ── Agent Playground surface classes ──────────────────────────────────────────────────────────
@@ -936,3 +937,59 @@ body {
936937
.ag-scroll-quiet::-webkit-scrollbar-thumb:hover {
937938
background-color: var(--ag-scroll-thumb-hover);
938939
}
940+
941+
/* Full-bleed rows inside a quiet scroller. Sticky headers must span the panel edge to edge,
942+
but the reserved gutter cuts them short, so let the scroller's children stretch across it
943+
and have the thumb overlay them. The gutter width is measured (it is platform-dependent,
944+
and zero on overlay-scrollbar systems) and published as --ag-scroll-gutter by
945+
useScrollbarGutterVar. */
946+
.ag-scroll-bleed {
947+
overflow-x: hidden;
948+
}
949+
950+
.ag-scroll-bleed > * {
951+
width: calc(100% + var(--ag-scroll-gutter, 0px));
952+
margin-inline-end: calc(-1 * var(--ag-scroll-gutter, 0px));
953+
}
954+
955+
/* Narrow agent config panel. The `config` container is declared on the config pane
956+
(`@container/config` in MainLayout), so these rules only fire for the panel and never for
957+
the same components elsewhere. Below 320px the panel drops every label it can spare and
958+
keeps the icons and the values, so it stays readable while the user drags it small. */
959+
.ag-config-title-short {
960+
display: none;
961+
}
962+
963+
@container config (max-width: 320px) {
964+
.ag-config-title-long {
965+
display: none;
966+
}
967+
968+
.ag-config-title-short {
969+
display: inline;
970+
}
971+
972+
/* Section rows keep the leading icon and the right-hand summary (the harness and model
973+
stay visible); only the title drops out. */
974+
.ag-section-title {
975+
display: none;
976+
}
977+
978+
/* Deploy and Commit become icon buttons. antd renders the label as a bare text node next
979+
to the icon span, so zeroing the button's font size is the only way to drop the text
980+
without changing every call site. The accessible name stays in the DOM. */
981+
.ag-config-actions .ant-btn {
982+
gap: 0;
983+
font-size: 0;
984+
}
985+
986+
.ag-config-actions .ant-btn .ant-btn-icon {
987+
font-size: 14px;
988+
}
989+
990+
/* List rows (tools, MCP servers) drop their second line and their transport tag, so the
991+
name itself survives instead of truncating to two characters. */
992+
.ag-row-secondary {
993+
display: none;
994+
}
995+
}

web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export function ItemRow({
125125
{descriptor.description ? (
126126
<Typography.Text
127127
type="secondary"
128-
className="block truncate text-xs leading-tight"
128+
className="ag-row-secondary block truncate text-xs leading-tight"
129129
>
130130
{descriptor.description}
131131
</Typography.Text>
@@ -134,7 +134,7 @@ export function ItemRow({
134134
<div className="flex shrink-0 items-center gap-1.5">
135135
{status ? <StatusTag status={status} /> : null}
136136
{descriptor.tags.map((tag) => (
137-
<Tag key={tag} className="m-0 text-[11px]">
137+
<Tag key={tag} className="ag-row-secondary m-0 text-[11px]">
138138
{tag}
139139
</Tag>
140140
))}
@@ -283,7 +283,10 @@ export function InstructionsFileRow({
283283
<span className="truncate font-mono text-[13px] font-medium leading-tight">
284284
{filename}
285285
</span>
286-
<Typography.Text type="secondary" className="shrink-0 text-[11px]">
286+
<Typography.Text
287+
type="secondary"
288+
className="ag-row-secondary shrink-0 text-[11px]"
289+
>
287290
{meta}
288291
</Typography.Text>
289292
{status ? <StatusTag status={status} /> : null}

web/packages/agenta-ui/src/components/presentational/section/ConfigAccordionSection.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,9 @@ export function ConfigAccordionSection({
271271
) : null}
272272
<Text
273273
className={cn(
274-
"min-w-0 truncate font-medium",
274+
// `ag-section-title` is a hook for narrow layouts that drop the title
275+
// and keep the icon + summary (see the `config` container query).
276+
"ag-section-title min-w-0 truncate font-medium",
275277
size === "compact" ? "text-xs" : "text-sm",
276278
)}
277279
>

0 commit comments

Comments
 (0)