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)