fix(web): cap the approval takeover at the composer's text height
The approval panel replaces the InputBar while a sandbox escalation waits, and its justification and command are unbounded model text. With no height cap, a long command grew the card until the refuse/allow row went under the fold: at 900x700 the action row's bottom landed at y=749, so the user could read the request and not answer it. Justification and command now scroll in one region capped at the same height as the composer's draft area, with the amber strip and the action row outside it. The cap is one value with two consumers — declared as --dsh-composer-text-max-height on ConversationRoot's .composerSeat, the composer chain's only shared ancestor — so the seat cannot cap its two states differently. The card rebinds the l2 scrollbar pair like every other scrolling surface on an elevated background. Covered by a new web e2e scenario that drives the real composition (read-only session, denied write, the model's escalation retry, answer clicked through the panel) and measures the live panel at two viewport heights against the composer's own cap, read off the textarea rather than hardcoded.
This commit is contained in:
@@ -19,6 +19,13 @@
|
||||
border-radius: 20px;
|
||||
background: var(--dsw-specific-input-major);
|
||||
box-shadow: var(--dsw-shadow-lv2);
|
||||
/* Elevated surface in dark, same as the menus: `.body` inside scrolls once
|
||||
the justification or command passes the cap, so the thumb takes the l2
|
||||
pair. Declared on the card because the elevation belongs to the surface,
|
||||
and the custom properties inherit down to the region that actually
|
||||
scrolls (see ui-theme styles/scrollbar.css for the rebinding contract). */
|
||||
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
||||
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
||||
}
|
||||
|
||||
/* Tinted full-width header band. */
|
||||
@@ -40,11 +47,22 @@
|
||||
background: var(--dsw-alias-state-warn-primary);
|
||||
}
|
||||
|
||||
/* Scroll region: an agent's justification and its command are unbounded model
|
||||
text (a one-line `cd` or a 40-line heredoc), and the seat sits in a
|
||||
fixed-height column — uncapped, a long command pushed the action row past
|
||||
the viewport and the approval could not be answered at all. The strip and
|
||||
the action row stay outside, so the buttons are always on screen. */
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 16px 14px;
|
||||
/* border-box so the cap is the region's OUTER height: the composer's draft
|
||||
area counts its padding inside the same number, and the two seats are
|
||||
only interchangeable if they occupy the same box. */
|
||||
box-sizing: border-box;
|
||||
max-height: var(--dsh-composer-text-max-height);
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px 0;
|
||||
}
|
||||
|
||||
/* The model's justification is the panel's message, not a footnote. */
|
||||
@@ -63,11 +81,13 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Card-level row, not body content: it carries the body's former bottom pad so
|
||||
the resting card keeps the draft's metrics while the scroll cap applies. */
|
||||
.actionRow {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px 14px;
|
||||
}
|
||||
|
||||
.allow,
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
// pending, this panel occupies the composer slot in place of the InputBar:
|
||||
// an amber "Waiting for approval" strip on the card top, the model's
|
||||
// justification as the headline, the paired command in muted code text, and
|
||||
// a right-aligned refuse/allow action row. One-shot: the buttons disable
|
||||
// a right-aligned refuse/allow action row. Justification and command are
|
||||
// unbounded model text, so they scroll inside the card at the shared composer
|
||||
// cap (`data-approval-scroll`) and the action row stays outside it — the
|
||||
// buttons must be reachable no matter how long the command is.
|
||||
// One-shot: the buttons disable
|
||||
// after a click and the panel leaves (the InputBar returns) on the broadcast
|
||||
// resolved frame. The draft's "Always allow this type" is deferred with
|
||||
// grant storage.
|
||||
@@ -53,17 +57,17 @@ function ApprovalFlow({ pending, command }: { pending: PendingApproval; command?
|
||||
<div className={css.root} data-approval-key={pending.key}>
|
||||
<div className={css.card}>
|
||||
<div className={css.strip}><span className={css.dot} />等待审批</div>
|
||||
<div className={css.body}>
|
||||
<div className={css.body} data-approval-scroll="">
|
||||
<div className={css.headline}>{pending.reason ?? `工具 ${pending.toolName} 请求越权执行`}</div>
|
||||
{command !== undefined && <div className={css.command}>{command}</div>}
|
||||
<div className={css.actionRow}>
|
||||
<button type="button" className={css.reject} disabled={answered} onClick={() => { answer('rejected') }}>
|
||||
拒绝
|
||||
</button>
|
||||
<button type="button" className={css.allow} disabled={answered} onClick={() => { answer('allowed-once') }}>
|
||||
允许一次
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.actionRow}>
|
||||
<button type="button" className={css.reject} disabled={answered} onClick={() => { answer('rejected') }}>
|
||||
拒绝
|
||||
</button>
|
||||
<button type="button" className={css.allow} disabled={answered} onClick={() => { answer('allowed-once') }}>
|
||||
允许一次
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +142,14 @@
|
||||
display: flex;
|
||||
flex: none;
|
||||
flex-direction: column;
|
||||
/* One cap for every scrolling text region a composer seat can hold: the
|
||||
InputBar draft (figma Input 75:8208 max 14 lines × 24px line) and the
|
||||
takeover panels' bodies top out at the same height, so electing a
|
||||
takeover never grows the footer past the card it replaces. Declared on
|
||||
the seat because it is the chain's only shared ancestor — fallback and
|
||||
elected overlay are siblings — and custom properties inherit down to
|
||||
whichever entry is mounted. */
|
||||
--dsh-composer-text-max-height: 336px;
|
||||
}
|
||||
|
||||
/* Active phase: header is ordinary column chrome above the scrollport (not
|
||||
|
||||
@@ -209,7 +209,9 @@
|
||||
.mirror {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
max-height: 336px;
|
||||
/* 14-line cap, shared with the composer takeovers (declared on
|
||||
ConversationRoot .composerSeat). */
|
||||
max-height: var(--dsh-composer-text-max-height);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user