fix(composer): show only approval card during authorization
This commit is contained in:
1 parent
b4757dd843
commit
4a7f17961d
4 files changed
+32
-34
No files matched your search
@@ -55,8 +55,7 @@ async function main(){
|
||||
id='e';status='pending';await render();await act(async()=>click('.conversation-intercept-card__rule-action'))
|
||||
check(document.querySelector('[role="status"]')?.textContent?.includes('未提供命中规则详情'),'missing rule explanation explicit')
|
||||
// Exercise the real composer with the real approval surface, not a skeleton.
|
||||
let session='session-a', active=true, sends=0, stops=0, changes=0, stopFails=false, request='request-1', stopPending=false
|
||||
let rejectStop!: (error: Error)=>void
|
||||
let session='session-a', active=true, sends=0, stops=0, changes=0, stopFails=false, request='request-1'
|
||||
let settled: 'answered'|'rejected'|undefined
|
||||
const draft='preserved draft'
|
||||
const attachments=[{id:'attachment-a',name:'a.txt',mimeType:'text/plain',size:1}]
|
||||
@@ -65,7 +64,7 @@ async function main(){
|
||||
const renderComposer=()=>act(async()=>{
|
||||
const composition=composeComposer({interaction:active?{id:session,kind:'approval',status:'pending'}:null,queueCount:2,running:true})
|
||||
root.render(<ComposerControls key={session} sessionId={session} value={draft} onChange={()=>{changes++}} onSend={()=>{sends++}}
|
||||
attachments={attachments} resources={resources} quotes={quotes} onStop={()=>{stops++;if(stopFails)throw new Error('stop fixture failed');if(stopPending)return new Promise<void>((_yes,no)=>{rejectStop=no})}}
|
||||
attachments={attachments} resources={resources} quotes={quotes} onStop={async()=>{stops++;if(stopFails)throw new Error('stop fixture failed')}}
|
||||
queueSlot={composition.queue.visible?<div data-queue="true">two queued messages</div>:undefined}
|
||||
approvalKey={request} approvalSlot={!composition.input.visible?<PermissionConfirm pendingId={session} toolName="Bash" options={options} resolve={pending} reject={async()=>{declined++}} onDecisionComplete={decision=>{settled=decision}}/>:undefined}/>)
|
||||
})
|
||||
@@ -75,6 +74,8 @@ async function main(){
|
||||
check(getComputedStyle(stack).display==='none','composer CSS removes the normal input from layout during approval')
|
||||
check(document.querySelectorAll('[data-composer-approval] [role="dialog"]').length===1,'one bottom approval surface')
|
||||
check(!document.querySelector('.wb-composer__toolbar'),'normal toolbar and send controls removed during approval')
|
||||
check(!document.querySelector('button[aria-label="停止当前运行"],button[aria-label="停止"],button[aria-label="发送"],.wb-composer__approval-stop'),'approval takeover has no standalone stop or send button even with onStop supplied')
|
||||
check(document.querySelector('[data-composer-approval]')?.children.length===1,'bottom approval surface contains only the approval card')
|
||||
check(!document.querySelector('[data-queue]'),'queue hidden without deleting queue state')
|
||||
const editor=stack.querySelector<HTMLElement>('[contenteditable]')!
|
||||
check(editor?.getAttribute('contenteditable')==='false','rich editor disabled during approval')
|
||||
@@ -99,18 +100,21 @@ async function main(){
|
||||
active=true;await renderComposer();await act(async()=>click('[data-option-id="once"]'));const staleResolve=resolve
|
||||
session='session-b';active=false;settled=undefined;await renderComposer();await act(async()=>staleResolve())
|
||||
check(!document.querySelector('[data-composer-approval]')&&settled===undefined,'session switch unmounts old request and ignores its late result')
|
||||
active=true;stopFails=true;await renderComposer();await act(async()=>click('.wb-composer__approval-stop'))
|
||||
check(document.querySelector('[data-composer-approval] [role="alert"]')?.textContent?.includes('stop fixture failed'),'failed cancellation visible in approval surface')
|
||||
request='request-2';stopFails=false;await renderComposer()
|
||||
check(!document.querySelector('[data-composer-approval] [role="alert"]'),'next request clears previous cancellation failure')
|
||||
await act(async()=>click('.wb-composer__approval-stop'))
|
||||
check(!document.querySelector('[data-composer-approval] [role="alert"]'),'cancellation retry clears error')
|
||||
check(stops===2,'stop remains reachable independently of normal send')
|
||||
stopPending=true;await renderComposer();await act(async()=>click('.wb-composer__approval-stop'))
|
||||
check(document.querySelector<HTMLButtonElement>('.wb-composer__approval-stop')?.disabled,'pending stop disabled against duplicate cancellation')
|
||||
request='request-3';stopPending=false;await renderComposer();await act(async()=>rejectStop(new Error('obsolete stop failure')))
|
||||
check(!document.querySelector('[data-composer-approval] [role="alert"]')&&!document.querySelector<HTMLButtonElement>('.wb-composer__approval-stop')?.disabled,'late previous-request cancellation cannot alter new request')
|
||||
active=false;await renderComposer();check(!document.querySelector('[data-composer-approval]'),'cancelled interaction restores ordinary input')
|
||||
// Stopping remains a normal-composer/parent action, never an extra approval control.
|
||||
stopFails=true;await renderComposer();await act(async()=>click('.wb-composer__queue-stop'))
|
||||
check(document.querySelector('[role="alert"]')?.textContent?.includes('stop fixture failed'),'normal composer still reports stop failure')
|
||||
stopFails=false;await act(async()=>click('.wb-composer__queue-stop'))
|
||||
check(stops===2,'normal composer stop remains reachable and can retry independently of send')
|
||||
active=true;await renderComposer()
|
||||
const previousApproval=document.querySelector('[data-composer-approval]')
|
||||
check(!document.querySelector('[data-composer-approval] [role="alert"]'),'normal stop failure does not leak into approval surface')
|
||||
check(!document.querySelector('button[aria-label="停止当前运行"],button[aria-label="停止"],.wb-composer__approval-stop'),'approval has no standalone cancellation control')
|
||||
request='request-2';await renderComposer()
|
||||
check(document.querySelector('[data-composer-approval]')!==previousApproval,'replacement request remounts keyed approval wrapper')
|
||||
check(stops===2&&sends===0,'approval takeover and replacement neither stop the run nor send a message')
|
||||
// Parent cancellation removes the pending interaction without depending on approval stop UI.
|
||||
active=false;await renderComposer();check(!document.querySelector('[data-composer-approval]'),'parent-cancelled interaction restores ordinary input')
|
||||
check(!!document.querySelector('.wb-composer__queue-stop')&&!!document.querySelector('[data-queue]'),'normal stop and queue restore after parent cancellation')
|
||||
// An upload already started before takeover must finish into the same draft without stealing focus.
|
||||
let uploaded: typeof attachments=[], stagingResolve!: (value: typeof attachments[number])=>void, uploadActive=false
|
||||
const uploadHost={pick:async()=>[],stage:()=>new Promise<typeof attachments[number]>(yes=>{stagingResolve=yes})}
|
||||
@@ -129,6 +133,15 @@ async function main(){
|
||||
check(!document.querySelector('.conversation-input-area__input-stack')!.contains(document.activeElement),'upload completion does not focus hidden editor')
|
||||
uploadActive=false;await renderUpload()
|
||||
check(document.querySelector('.conversation-input-area__input-stack')?.textContent?.includes('upload.txt'),'completed attachment restored after approval')
|
||||
// Empty drafts restore the ordinary stop button, not a send button, after takeover.
|
||||
const renderEmpty=(approval:boolean)=>act(async()=>root.render(<ComposerControls key="empty-session" sessionId="empty-session" value="" onChange={()=>{}} onSend={()=>{sends++}} onStop={async()=>{stops++}}
|
||||
approvalKey="empty-approval" approvalSlot={approval?<PermissionConfirm pendingId="empty-approval" toolName="Bash" options={options} resolve={pending} reject={async()=>{}}/>:undefined}/>))
|
||||
await renderEmpty(true)
|
||||
check(!document.querySelector('button[aria-label="停止"],button[aria-label="发送"]'),'empty draft also has no normal stop or send during approval')
|
||||
await renderEmpty(false)
|
||||
check(!!document.querySelector('button[aria-label="停止"]')&&!document.querySelector('button[aria-label="发送"]'),'empty draft restores normal stop without send after approval')
|
||||
await act(async()=>click('button[aria-label="停止"]'))
|
||||
check(stops===3&&sends===0,'restored empty-draft stop still invokes cancellation without sending')
|
||||
await act(async()=>root.unmount());dom.window.close()
|
||||
console.log(`Permission intercept DOM: ${checks} checks passed (not pixel/SDK verification)`)
|
||||
process.exit(0)
|
||||
|
||||
@@ -146,24 +146,10 @@ export function readReferenceUris(transfer: Pick<DataTransfer, 'getData'>): stri
|
||||
return transfer.getData('text/uri-list').split(/\r\n?|\n/).filter((value) => value.trim() && !value.startsWith('#'))
|
||||
}
|
||||
|
||||
/** Keyed by request identity so a late cancellation result cannot affect its successor. */
|
||||
function ApprovalComposerSurface({ children, onStop }: { children: ReactNode; onStop?: () => void | Promise<void> }): React.JSX.Element {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const busyRef = useRef(false)
|
||||
const mounted = useRef(true)
|
||||
useEffect(() => { mounted.current = true; return () => { mounted.current = false } }, [])
|
||||
/** Keep each approval subtree scoped to its request identity. */
|
||||
function ApprovalComposerSurface({ children }: { children: ReactNode }): React.JSX.Element {
|
||||
return <div className="conversation-input-area__approval" data-composer-approval="true">
|
||||
{children}
|
||||
{onStop && <button type="button" className="wb-composer__approval-stop" aria-label="停止当前运行" disabled={busy} onClick={async () => {
|
||||
if (busyRef.current) return
|
||||
busyRef.current = true
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
try { await onStop() } catch (reason) { if (mounted.current) setError(errorText(reason)) }
|
||||
finally { if (mounted.current) { busyRef.current = false; setBusy(false) } }
|
||||
}}>停止当前运行</button>}
|
||||
{error && <div className="wb-composer__error" role="alert">{error}</div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -556,7 +542,7 @@ export function ComposerControls({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{approvalTakeover && <ApprovalComposerSurface key={approvalKey ?? sessionId} onStop={onStop}>{approvalSlot}</ApprovalComposerSurface>}
|
||||
{approvalTakeover && <ApprovalComposerSurface key={approvalKey ?? sessionId}>{approvalSlot}</ApprovalComposerSurface>}
|
||||
{!approvalTakeover && floatingSlot && <div className="conversation-input-area__custom-floating">{floatingSlot}</div>}
|
||||
<div ref={inputStackRef} hidden={approvalTakeover} inert={approvalTakeover} aria-hidden={approvalTakeover || undefined}
|
||||
onFocusCapture={event => { if (approvalTakeover) { event.stopPropagation(); (event.target as HTMLElement).blur() } }}
|
||||
|
||||
@@ -550,4 +550,3 @@
|
||||
/* B03: approval owns the bottom surface; keep draft state mounted, never visible/focusable. */
|
||||
.wb-composer .conversation-input-area__input-stack[hidden] { display: none; }
|
||||
.conversation-input-area__approval { width: 100%; min-width: 0; }
|
||||
.wb-composer__approval-stop { display: block; margin: var(--yc-space-3) 0 0 auto; cursor: pointer; }
|
||||
@@ -137,7 +137,7 @@ function panelId(value: ComposerInteractionSnapshot | ComposerNoticeSnapshot | C
|
||||
* data.
|
||||
* 4. Tasks move into the queue header when the queue is visible, otherwise
|
||||
* they stay in the unified stack above the editor.
|
||||
* 5. The running generation keeps its stop action reachable in every state.
|
||||
* 5. Running state keeps its stop action semantics; approval takeover hides the normal toolbar and renders only the approval card.
|
||||
*/
|
||||
export function composeComposer(input: ComposerCompositionInput = {}): ComposerComposition {
|
||||
const interaction = isActiveComposerInteraction(input.interaction) ? input.interaction : null
|
||||
|
||||
Reference in new issue
Block a user