Merge remote-tracking branch 'origin/master' into mergebot/pr883

# Conflicts:
#	packages/client/tsdown.client.ts
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/PendingCard.tsx
#	packages/client/ui-conversation/src/client/chat/ToolRow.tsx
#	packages/client/ui-conversation/src/client/toolviews/todo-row.module.css
#	packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
#	packages/client/ui-conversation/tests/chat-apply.spec.tsx
#	packages/client/ui-conversation/tests/coverage-tails.spec.tsx
#	packages/client/ui-conversation/tests/todo-panel.spec.tsx
#	packages/client/ui-primitives/src/icons/index.tsx
#	packages/client/ui-primitives/tests/icons.spec.tsx
#	packages/client/ui-question/README.i18n.yaml
#	packages/client/ui-question/src/client/QuestionComposer.module.css
#	packages/client/ui-question/src/client/QuestionComposer.tsx
This commit is contained in:
imccyu
2026-07-30 01:01:14 +08:00
1663 changed files with 59630 additions and 13353 deletions
@@ -24,6 +24,13 @@
box-shadow: var(--dsw-shadow-lv2);
color: var(--dsw-alias-label-primary);
overflow: hidden;
/* Elevated surface in dark, same as the menus: the body inside scrolls
once the card hits the cap above, so the thumb takes the l2 pair. Declared
on the card because the elevation belongs to the surface, and the custom
properties inherit down to `.body` (see ui-theme styles/scrollbar.css
for the rebinding contract). */
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
.card,
@@ -62,11 +69,7 @@
}
.detail {
margin: 2px 0 0;
color: var(--dsw-alias-label-tertiary);
font-size: 13px;
line-height: 20px;
font-weight: 400;
margin: 0 2px 8px;
}
.footerActions {
@@ -117,15 +120,21 @@
cursor: default;
}
.body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
.options {
display: flex;
flex-direction: column;
gap: 1px;
margin: 8px 0 0;
padding: 4px 12px;
/* The scrollable region of the capped card (ChatView list pattern). */
min-height: 0;
overflow-y: auto;
}
.option {
@@ -2,7 +2,7 @@ import { useMemo, useState, type KeyboardEvent } from 'react'
import clsx from 'clsx'
import {
Button, IconCheckOutline14, IconChevronLeftOutline14, IconChevronRightOutline14,
IconCloseOutline16, IconEditOutline16,
IconCloseOutline16, IconEditOutline16, MarkdownText,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { LocaleSnapshot, Translate } from '@deepseek-ai/dsh-client-locale/client'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
@@ -171,7 +171,6 @@ function QuestionFlow({ pending, t, useLocale }: {
<h2 className={css.title} id={`question-${pending.key}-${String(index)}`}>
{question.question}
</h2>
{question.detail !== undefined && <p className={css.detail}>{question.detail}</p>}
</div>
<button
type="button" className={css.iconButton} aria-label={t('dismiss')}
@@ -182,67 +181,94 @@ function QuestionFlow({ pending, t, useLocale }: {
</button>
</header>
<div className={css.options} role={question.multiSelect === true ? 'group' : 'radiogroup'}>
{(question.options ?? []).map((option, optionIndex) => {
const selected = draft.selected.includes(option.label)
const display = parseRecommendedLabel(option.label)
return (
<button
type="button" key={`${option.label}-${String(optionIndex)}`}
className={clsx(css.option, selected && question.multiSelect !== true && css.optionSelected)}
role={question.multiSelect === true ? 'checkbox' : 'radio'}
aria-checked={selected}
aria-label={display.label}
disabled={busy !== null}
onClick={() => { choose(option.label) }}
onKeyDown={(event) => {
if (event.key !== 'Enter' || !drafts.every(completed)) return
event.preventDefault()
submitDrafts(drafts)
}}
>
{question.multiSelect === true
? (
<span className={clsx(css.checkbox, selected && css.checkboxChecked)} aria-hidden="true">
{selected && <IconCheckOutline14 size={12} />}
<div className={css.body} data-question-scroll>
{question.detail !== undefined && (
<div className={css.detail}><MarkdownText text={question.detail} /></div>
)}
<div className={css.options} role={question.multiSelect === true ? 'group' : 'radiogroup'}>
{(question.options ?? []).map((option, optionIndex) => {
const selected = draft.selected.includes(option.label)
const display = parseRecommendedLabel(option.label)
return (
<button
type="button" key={`${option.label}-${String(optionIndex)}`}
className={clsx(css.option, selected && question.multiSelect !== true && css.optionSelected)}
role={question.multiSelect === true ? 'checkbox' : 'radio'}
aria-checked={selected}
aria-label={display.label}
disabled={busy !== null}
onClick={() => { choose(option.label) }}
onKeyDown={(event) => {
if (event.key !== 'Enter' || !drafts.every(completed)) return
event.preventDefault()
submitDrafts(drafts)
}}
>
{question.multiSelect === true
? (
<span className={clsx(css.checkbox, selected && css.checkboxChecked)} aria-hidden="true">
{selected && <IconCheckOutline14 size={12} />}
</span>
)
: <span className={css.number}>{optionIndex + 1}</span>}
<span className={css.optionCopy}>
<span className={css.optionLine}>
<span className={css.optionLabel}>{display.label}</span>
{display.recommended && <span className={css.badge}>{t('option.recommended')}</span>}
{option.description !== undefined && (
<span className={css.description}>{option.description}</span>
)}
</span>
)
: <span className={css.number}>{optionIndex + 1}</span>}
<span className={css.optionCopy}>
<span className={css.optionLine}>
<span className={css.optionLabel}>{display.label}</span>
{display.recommended && <span className={css.badge}>{t('option.recommended')}</span>}
{option.description !== undefined && (
<span className={css.description}>{option.description}</span>
)}
</span>
</span>
</button>
)
})}
</button>
)
})}
{hasOptions
? (
<div className={clsx(css.customRow, draft.custom !== '' && css.customRowActive)}>
{question.multiSelect === true
? (
<span
className={clsx(css.checkbox, draft.custom !== '' && css.checkboxChecked)}
aria-hidden="true"
>
{draft.custom !== '' && <IconCheckOutline14 size={12} />}
</span>
)
: (
<span className={css.number} aria-hidden="true">
<IconEditOutline16 size={12} />
</span>
)}
<input
type="text"
className={css.customInput}
{hasOptions
? (
<div className={clsx(css.customRow, draft.custom !== '' && css.customRowActive)}>
{question.multiSelect === true
? (
<span
className={clsx(css.checkbox, draft.custom !== '' && css.checkboxChecked)}
aria-hidden="true"
>
{draft.custom !== '' && <IconCheckOutline14 size={12} />}
</span>
)
: (
<span className={css.number} aria-hidden="true">
<IconEditOutline16 size={12} />
</span>
)}
<input
type="text"
className={css.customInput}
value={draft.custom}
disabled={busy !== null}
placeholder={t('custom.placeholder')}
onChange={(event) => {
const value = event.target.value
updateDraft(current => ({
...current, selected: [], custom: value, skipped: false,
}))
}}
onKeyDown={(event) => {
if (event.key === 'Enter' && !isComposing(event)) {
event.preventDefault()
continueFlow()
}
}}
/>
</div>
)
: (
<textarea
autoFocus
className={css.customTextarea}
value={draft.custom}
disabled={busy !== null}
rows={2}
placeholder={t('custom.placeholder')}
onChange={(event) => {
const value = event.target.value
@@ -251,36 +277,14 @@ function QuestionFlow({ pending, t, useLocale }: {
}))
}}
onKeyDown={(event) => {
if (event.key === 'Enter' && !isComposing(event)) {
if (event.key === 'Enter' && !event.shiftKey && !isComposing(event)) {
event.preventDefault()
continueFlow()
}
}}
/>
</div>
)
: (
<textarea
autoFocus
className={css.customTextarea}
value={draft.custom}
disabled={busy !== null}
rows={2}
placeholder={t('custom.placeholder')}
onChange={(event) => {
const value = event.target.value
updateDraft(current => ({
...current, selected: [], custom: value, skipped: false,
}))
}}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey && !isComposing(event)) {
event.preventDefault()
continueFlow()
}
}}
/>
)}
)}
</div>
</div>
<footer className={css.footer}>