refactor(desktop): share the clarify choice row and fix skip copy

Builds on the skipped-clarify card so it holds up beyond the timeout case:

- Extract a shared `ChoiceButton` (letter badge + label + row chrome) used
  by both the live pending card and the settled skip card. The two blocks
  had drifted into duplicated markup; now they can't diverge.
- Fix the hint copy. An empty `user_response` is emitted for BOTH a
  server-side timeout AND a manual Skip (tools/clarify_tool.py) — there is
  no field on the result that tells them apart — so asserting "This question
  timed out" was wrong half the time. Neutral wording ("This prompt is no
  longer waiting…") is correct for either, and the recover-your-answer path
  now also helps someone who mis-clicked Skip. Updated en/ja/zh/zh-hant.

No behavior change to the live prompt or the follow-up-message flow.

Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-22 21:39:27 -05:00
parent 8b96fc57ed
commit efa002dd5d
5 changed files with 50 additions and 27 deletions

View file

@ -126,6 +126,43 @@ function KeyBadge({ char, preview, selected }: { char: string; preview?: boolean
)
}
/** A letter-badged option row. Shared by the live pending card (where a click
* selects an answer) and the settled skip card (where a click drafts a
* follow-up), so both stay visually identical. */
function ChoiceButton({
char,
choice,
disabled,
onClick,
selected = false,
title
}: {
char: string
choice: string
disabled?: boolean
onClick: () => void
selected?: boolean
title?: string
}) {
return (
<button
className={cn(
OPTION_ROW_CLASS,
'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-(--ui-text-primary)',
selected && 'text-(--ui-text-primary)'
)}
data-choice
disabled={disabled}
onClick={onClick}
title={title}
type="button"
>
<KeyBadge char={char} selected={selected} />
<span className="flex-1 wrap-anywhere">{choice}</span>
</button>
)
}
export const ClarifyTool = (props: ToolCallMessagePartProps) => {
// Answered → settled Q&A (ToolFallback collapsed the answer away).
if (props.result !== undefined) {
@ -198,20 +235,13 @@ function ClarifyToolSettled({ args, result }: ToolCallMessagePartProps) {
{skipped && choices.length > 0 ? (
<div className="grid gap-px" data-clarify-late-choices="" role="group">
{choices.map((choice, index) => (
<button
className={cn(
OPTION_ROW_CLASS,
'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-(--ui-text-primary)'
)}
data-choice
<ChoiceButton
char={letterFor(index)}
choice={choice}
key={`${index}-${choice}`}
onClick={() => followUp(choice)}
title={copy.lateAnswerTip}
type="button"
>
<KeyBadge char={letterFor(index)} selected={false} />
<span className="flex-1 wrap-anywhere">{choice}</span>
</button>
/>
))}
<p className="px-1.5 pt-0.5 text-[0.6875rem] leading-4 text-(--ui-text-tertiary)">{copy.lateAnswerHint}</p>
</div>
@ -423,21 +453,14 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
{hasChoices ? (
<div className="grid gap-px" role="group">
{choices.map((choice, index) => (
<button
className={cn(
OPTION_ROW_CLASS,
'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-(--ui-text-primary)',
selectedChoice === choice && 'text-(--ui-text-primary)'
)}
data-choice
<ChoiceButton
char={letterFor(index)}
choice={choice}
disabled={submitting}
key={`${index}-${choice}`}
onClick={() => selectChoice(choice)}
type="button"
>
<KeyBadge char={letterFor(index)} selected={selectedChoice === choice} />
<span className="flex-1 wrap-anywhere">{choice}</span>
</button>
selected={selectedChoice === choice}
/>
))}
<label className={cn(OPTION_ROW_CLASS, 'items-center')}>
<KeyBadge char={letterFor(choices.length)} preview={otherFocused} selected={Boolean(trimmedDraft)} />

View file

@ -2527,7 +2527,7 @@ export const en: Translations = {
continueLabel: 'Continue',
lateAnswer: (question, choice) => `Re: "${question}" — my answer: ${choice}`,
lateAnswerTip: 'Draft this answer as a follow-up message',
lateAnswerHint: 'This question timed out. Picking an option drafts it as a follow-up message.'
lateAnswerHint: 'This prompt is no longer waiting. Pick an option to draft it as a follow-up message.'
},
tool: {
code: 'Code',

View file

@ -2457,7 +2457,7 @@ export const ja = defineLocale({
continueLabel: '続行',
lateAnswer: (question, choice) => `${question}」について — 私の回答: ${choice}`,
lateAnswerTip: 'この回答をフォローアップメッセージとして下書きします',
lateAnswerHint: 'この質問はタイムアウトしました。選択肢を選ぶとフォローアップメッセージとして下書きされます。'
lateAnswerHint: 'この質問はもう回答を待っていません。選択肢を選ぶとフォローアップメッセージとして下書きされます。'
},
tool: {
code: 'コード',

View file

@ -2383,7 +2383,7 @@ export const zhHant = defineLocale({
continueLabel: '繼續',
lateAnswer: (question, choice) => `關於「${question}」 — 我的回答: ${choice}`,
lateAnswerTip: '將此回答起草為後續訊息',
lateAnswerHint: '此問題已逾時。選擇一個選項會將其起草為後續訊息。'
lateAnswerHint: '此問題已不再等待回答。選擇一個選項會將其起草為後續訊息。'
},
tool: {
code: '程式碼',

View file

@ -2699,7 +2699,7 @@ export const zh: Translations = {
continueLabel: '继续',
lateAnswer: (question, choice) => `关于"${question}" — 我的回答: ${choice}`,
lateAnswerTip: '将此回答起草为后续消息',
lateAnswerHint: '此问题已超时。选择一个选项会将其起草为后续消息。'
lateAnswerHint: '此问题已不再等待回答。选择一个选项会将其起草为后续消息。'
},
tool: {
code: '代码',