fix(web): expose plan selector state accessibly
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Web plan-mode feature with two lifecycle-coupled halves. The node entry mounts `@deepseek-ai/dsh-plan-mode` with the Web product policy; the browser entry contributes a session-scoped selector to `conversation.composer.controls`.
|
||||
|
||||
The selector distinguishes unavailable capability (`planMode === null`), committed mode (`active`), and the target queued for the next model-request boundary (`pending`, including `pending: false`). Selecting a mode never cancels a running turn. It remains available while generation is running, disables only during its own RPC, and displays the host-confirmed pending target until a logged `plan/mode` event commits it.
|
||||
The selector distinguishes unavailable capability (`planMode === null`), committed mode (`active`), and the target queued for the next model-request boundary (`pending`, including `pending: false`). Selecting a mode never cancels a running turn. It remains available while generation is running, disables only during its own RPC, and displays the host-confirmed pending target until a logged `plan/mode` event commits it. The transparent native select mirrors keyboard focus onto the visible chip and carries a dynamic accessible description of the committed and pending modes.
|
||||
|
||||
The model exits plan mode through the stable `exit_plan_mode` tool. Its plan review uses the composed Web question channel: approval schedules default mode for the next step, while rejection or custom feedback keeps plan mode active and returns the feedback to the model.
|
||||
|
||||
|
||||
@@ -29,10 +29,27 @@
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.root:focus-within .chip {
|
||||
outline: 2px solid var(--dsw-alias-brand-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.description {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.select {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import type { PlanModeControlProps } from './index.ts'
|
||||
import css from './PlanModeControl.module.css'
|
||||
|
||||
@@ -13,6 +13,7 @@ export function PlanModeControl({ useSession, setPlanMode }: PlanModeControlProp
|
||||
const [switching, setSwitching] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const aliveRef = useRef(true)
|
||||
const descriptionId = useId()
|
||||
|
||||
useEffect(() => {
|
||||
aliveRef.current = true
|
||||
@@ -57,9 +58,11 @@ export function PlanModeControl({ useSession, setPlanMode }: PlanModeControlProp
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
</span>
|
||||
<span id={descriptionId} className={css.description}>{title}</span>
|
||||
<select
|
||||
className={css.select}
|
||||
aria-label="协作模式"
|
||||
aria-describedby={descriptionId}
|
||||
value={value}
|
||||
disabled={switching}
|
||||
onChange={(event) => { select(event.target.value === 'plan') }}
|
||||
|
||||
@@ -40,17 +40,26 @@ describe('PlanModeControl', () => {
|
||||
cleanup()
|
||||
setup({ active: false })
|
||||
expect(screen.getByTitle('当前为默认模式')).toBeTruthy()
|
||||
expect((screen.getByRole('combobox', { name: '协作模式' }) as HTMLSelectElement).value).toBe('default')
|
||||
const select = screen.getByRole('combobox', { name: '协作模式' }) as HTMLSelectElement
|
||||
expect(select.value).toBe('default')
|
||||
expect(document.getElementById(select.getAttribute('aria-describedby') ?? '')?.textContent)
|
||||
.toBe('当前为默认模式')
|
||||
})
|
||||
|
||||
it('treats pending field presence as the target, including pending false', () => {
|
||||
setup({ active: false, pending: true })
|
||||
expect(screen.getByText('计划 · 待生效')).toBeTruthy()
|
||||
expect(screen.getByTitle(/当前为默认模式/)).toBeTruthy()
|
||||
const planSelect = screen.getByRole('combobox')
|
||||
expect(document.getElementById(planSelect.getAttribute('aria-describedby') ?? '')?.textContent)
|
||||
.toBe('当前为默认模式;计划模式将在下一次模型请求时生效')
|
||||
cleanup()
|
||||
setup({ active: true, pending: false })
|
||||
expect(screen.getByText('默认 · 待生效')).toBeTruthy()
|
||||
expect((screen.getByRole('combobox') as HTMLSelectElement).value).toBe('default')
|
||||
const defaultSelect = screen.getByRole('combobox') as HTMLSelectElement
|
||||
expect(defaultSelect.value).toBe('default')
|
||||
expect(document.getElementById(defaultSelect.getAttribute('aria-describedby') ?? '')?.textContent)
|
||||
.toBe('当前为计划模式;默认模式将在下一次模型请求时生效')
|
||||
})
|
||||
|
||||
it('switches from the effective target and remains available while a turn runs', async () => {
|
||||
|
||||
Reference in New Issue
Block a user