mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(web): expose plan selector state accessibly
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-24-web-plan-mode.md: 42407198c09c34f2ddde559f5ddeae8d7736169e
|
||||
2026-07-24-web-plan-mode.zh.md: db865a26e91977670ed520aecf9d683a6bea5dab
|
||||
2026-07-24-web-plan-mode.md: 3b8a14a279dee478a291b19d5c0aed82f5a16c5c
|
||||
2026-07-24-web-plan-mode.zh.md: ca52918257c289fab966f1915348b08de45d86f1
|
||||
|
||||
@@ -18,7 +18,7 @@ The policy is product-owned configuration at this composition boundary. It tells
|
||||
|
||||
`ui-conversation` owns and renders the new additive controls slot to the left of the primary composer action. It provides no business payload; entries receive the standard session kit. Whole-composer replacements remain on the separate selector-routed `conversation.composer` chain, so a pending question replaces the InputBar and its controls without either feature importing the other.
|
||||
|
||||
The control is absent when `planMode` is `null`. Otherwise, its selected value is `pending ?? active`, while pending presentation tests field presence rather than truthiness. It displays `计划 · 待生效` or `默认 · 待生效` until a logged commit replaces the snapshot. A selection disables only the selector while its own RPC is in flight. Generation does not disable it: selecting during a running turn neither calls cancel nor changes that turn, and cancelling generation does not clear the pending target.
|
||||
The control is absent when `planMode` is `null`. Otherwise, its selected value is `pending ?? active`, while pending presentation tests field presence rather than truthiness. It displays `计划 · 待生效` or `默认 · 待生效` until a logged commit replaces the snapshot. The transparent native select mirrors focus onto the visible chip and references a dynamic accessible description that distinguishes committed mode from the pending target. A selection disables only the selector while its own RPC is in flight. Generation does not disable it: selecting during a running turn neither calls cancel nor changes that turn, and cancelling generation does not clear the pending target.
|
||||
|
||||
## Interaction semantics
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p
|
||||
|
||||
`ui-conversation` 拥有并渲染新增的可叠加控件槽,其位置在 composer 主操作左侧。该槽不提供业务载荷;各入口接收标准会话注入项。替换整个 composer 的功能仍走另一条由选择器路由的 `conversation.composer` 链,因此待处理的问题会替换 InputBar 及其控件,两个功能均无需导入对方。
|
||||
|
||||
当 `planMode` 为 `null` 时,控件不会出现。否则,其选中值为 `pending ?? active`,而待生效状态的呈现依据是字段是否存在,而非字段真值。在日志提交替换快照前,控件会显示 `计划 · 待生效` 或 `默认 · 待生效`。选择操作只会在自身 RPC 执行期间禁用选择器。生成过程不会禁用该控件:在运行中的轮次里选择模式,既不会调用取消,也不会改变该轮次;取消生成也不会清除待生效目标。
|
||||
当 `planMode` 为 `null` 时,控件不会出现。否则,其选中值为 `pending ?? active`,而待生效状态的呈现依据是字段是否存在,而非字段真值。在日志提交替换快照前,控件会显示 `计划 · 待生效` 或 `默认 · 待生效`。透明的原生 select 会将焦点状态映射到可见的 chip,并引用一条动态无障碍描述,以区分已提交模式和待生效目标。选择操作只会在自身 RPC 执行期间禁用选择器。生成过程不会禁用该控件:在运行中的轮次里选择模式,既不会调用取消,也不会改变该轮次;取消生成也不会清除待生效目标。
|
||||
|
||||
## 交互语义
|
||||
|
||||
|
||||
@@ -252,9 +252,20 @@ describe('web boot chain success pass (keyless, ten real bundles, ?fixture)', ()
|
||||
const select = page.getByRole('combobox', { name: '协作模式' })
|
||||
await select.waitFor({ state: 'visible', timeout: 15_000 })
|
||||
await expect(page.getByTitle('当前为默认模式').isVisible()).resolves.toBe(true)
|
||||
await select.focus()
|
||||
expect(await select.evaluate((element) => {
|
||||
const chip = element.parentElement?.firstElementChild
|
||||
if (!(chip instanceof HTMLElement)) return null
|
||||
const style = getComputedStyle(chip)
|
||||
return { outlineStyle: style.outlineStyle, outlineWidth: style.outlineWidth }
|
||||
})).toEqual({ outlineStyle: 'solid', outlineWidth: '2px' })
|
||||
|
||||
await select.selectOption('plan')
|
||||
await page.getByTitle(/计划模式将在下一次模型请求时生效/).waitFor()
|
||||
const descriptionId = await select.getAttribute('aria-describedby')
|
||||
expect(descriptionId).not.toBeNull()
|
||||
expect(await page.locator(`[id="${descriptionId}"]`).textContent())
|
||||
.toBe('当前为默认模式;计划模式将在下一次模型请求时生效')
|
||||
const input = page.locator('textarea[placeholder]')
|
||||
await input.fill('commit plan mode')
|
||||
await page.getByRole('button', { name: '发送' }).click()
|
||||
|
||||
@@ -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