diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index b4c04c67bb..43a0f3e1ec 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -285,13 +285,14 @@ export function InputBar({ } const ordinary = subagent === null - const primaryLabel = running && ordinary ? t('input.stop') : t('input.send') + const stopping = running && ordinary + const primaryLabel = stopping ? t('input.stop') : t('input.send') const onPrimary = (): void => { - if (inputActions === undefined || stop === undefined) return // absent machine: the button is disabled - if (running && ordinary) { - stop() + if (stopping) { + stop?.() return } + if (inputActions === undefined) return // absent machine: the button is disabled /* v8 ignore next -- defensive: the primary button is disabled while empty||disabled, so a click cannot reach the false arm. */ if (!empty && !disabled && !machineBusy) inputActions.submit() } @@ -467,11 +468,11 @@ export function InputBar({ className={css.primary} aria-label={primaryLabel} title={primaryLabel} - disabled={!running && (empty || disabled || machineBusy)} + disabled={stopping ? stop === undefined : empty || disabled || machineBusy} onMouseDown={keepFocus} onClick={onPrimary} > - {running ? ( + {stopping ? ( diff --git a/packages/client/ui-conversation/tests/input-bar.spec.tsx b/packages/client/ui-conversation/tests/input-bar.spec.tsx index fb82f9a4e8..2072fd2c90 100644 --- a/packages/client/ui-conversation/tests/input-bar.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.spec.tsx @@ -41,6 +41,7 @@ interface BenchOptions { permissions?: { options: { value: string; name: string; description?: string }[]; currentValue: string } draft?: string running?: boolean + subagent?: Exclude disabled?: boolean promptError?: ConversationSnapshot['promptError'] variant?: 'hero' | 'composer' @@ -76,6 +77,7 @@ function bench(over?: BenchOptions) { if (over?.draft !== undefined && over.draft !== '') shell.setDraft(over.draft) const session = createSnapshotStore(snapshotOf({ running: over?.running ?? false, + subagent: over?.subagent ?? null, removed: over?.disabled ?? false, promptError: over?.promptError ?? null, })) @@ -124,8 +126,9 @@ function bench(over?: BenchOptions) { const view = render() const textarea = view.container.querySelector('textarea')! // aria-label (not role name): title carries the same label and would double-match. + const stopping = over?.running === true && over.subagent === undefined const button = view.container.querySelector( - `button[aria-label="${over?.running === true ? '停止生成' : '发送消息'}"]`, + `button[aria-label="${stopping ? '停止生成' : '发送消息'}"]`, )! return { view, textarea, button, props, sink, shell, wiring: shell, session, stop, slotCalls, menuLauncher } } @@ -208,6 +211,38 @@ describe('running and lock semantics (queue cut 1)', () => { expect(stop).toHaveBeenCalledTimes(1) }) + it('running subagent primary admits a follow-up instead of exposing Stop', () => { + const { button, sink, stop } = bench({ + running: true, + draft: '后续消息', + subagent: { + address: { + parentSessionId: 'parent' as SessionId, + childSessionId: SID, + mode: 'continuable', + }, + parentAvailable: true, + }, + }) + expect(button.getAttribute('aria-label')).toBe('发送消息') + fireEvent.click(button) + expect(sink).toHaveBeenCalledWith('后续消息') + expect(stop).not.toHaveBeenCalled() + + const empty = bench({ + running: true, + subagent: { + address: { + parentSessionId: 'parent' as SessionId, + childSessionId: SID, + mode: 'continuable', + }, + parentAvailable: true, + }, + }) + expect(empty.button.disabled).toBe(true) + }) + it('disabled (session removed) locks the textarea and chrome', () => { const { textarea, view } = bench({ disabled: true }) expect(textarea.disabled).toBe(true)