feat(desktop): persistent needs-input indicator + icon button consolidation
Replace the background-clarify toast (expired on alt-tab, easy to miss) with a persistent, glowing amber "needs input" dot on the session's sidebar row, driven off a new ClientSessionState.needsInput flag mirrored into a $attentionSessionIds store. The flag is set on clarify.request and cleared the moment the turn resumes (tool.complete) or ends. Also: redesign the clarify tool UI (borderless choices, pseudo-radio dots, right-aligned checkmark, arc border, tighter padding), make Button the single source of icon-button styling (4px radius, new icon-titlebar variant, titlebar buttons rendered polymorphically via asChild, Codicons throughout), put the file-tree refresh action first, and .trim() pasted composer text.
This commit is contained in:
@@ -8,7 +8,7 @@ import { ToolFallback } from '@/components/assistant-ui/tool-fallback'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { HelpCircle, Loader2, PencilLine } from '@/lib/icons'
|
||||
import { Check, HelpCircle, Loader2 } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $clarifyRequest, clearClarifyRequest } from '@/store/clarify'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
@@ -74,6 +74,7 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
|
||||
const [typing, setTyping] = useState(false)
|
||||
const [draft, setDraft] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// Race: tool.start fires a tick before clarify.request, so request_id
|
||||
@@ -140,71 +141,59 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
|
||||
[draft, respond]
|
||||
)
|
||||
|
||||
const handleChoiceKey = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (typing || submitting) {
|
||||
return
|
||||
}
|
||||
|
||||
const numeric = Number.parseInt(event.key, 10)
|
||||
|
||||
if (Number.isFinite(numeric) && numeric >= 1 && numeric <= choices.length) {
|
||||
event.preventDefault()
|
||||
void respond(choices[numeric - 1]!)
|
||||
}
|
||||
},
|
||||
[choices, respond, submitting, typing]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-3 mt-2 grid gap-3 rounded-xl border border-border/70 bg-card/40 px-4 py-3.5 text-sm',
|
||||
'shadow-[inset_0_1px_0_color-mix(in_srgb,var(--foreground)_3%,transparent)]'
|
||||
)}
|
||||
className="relative mb-3 mt-2 grid gap-2 rounded-[0.5rem] border border-border/70 bg-card/40 px-3 py-2.5 text-sm shadow-[inset_0_1px_0_color-mix(in_srgb,var(--foreground)_3%,transparent)]"
|
||||
data-slot="clarify-inline"
|
||||
>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<span aria-hidden className="arc-border" />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
aria-hidden
|
||||
className="mt-0.5 grid size-6 shrink-0 place-items-center rounded-md bg-[color-mix(in_srgb,var(--dt-primary)_14%,transparent)] text-primary ring-1 ring-inset ring-primary/15"
|
||||
className="grid size-6 shrink-0 place-items-center rounded-md bg-[color-mix(in_srgb,var(--dt-primary)_14%,transparent)] text-primary ring-1 ring-inset ring-primary/15"
|
||||
>
|
||||
<HelpCircle className="size-3.5" />
|
||||
</span>
|
||||
<div className="grid flex-1 gap-0.5">
|
||||
<span className="text-[0.6875rem] font-medium uppercase tracking-wide text-muted-foreground/85">
|
||||
Hermes is asking
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap leading-snug text-foreground">
|
||||
{question || <em className="text-muted-foreground/70">Loading question…</em>}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-1 whitespace-pre-wrap font-medium leading-snug text-foreground">
|
||||
{question || <em className="font-normal text-muted-foreground/70">Loading question…</em>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!typing && hasChoices && (
|
||||
<div className="grid gap-1.5" onKeyDown={handleChoiceKey} role="group">
|
||||
<div className="grid gap-0.5" role="group">
|
||||
{choices.map((choice, index) => (
|
||||
<button
|
||||
className={cn(
|
||||
'group/choice flex w-full items-center gap-3 rounded-lg border border-border/70 bg-background/60 px-3 py-2 text-left text-sm text-foreground/95',
|
||||
'transition-colors hover:border-border hover:bg-accent/60 disabled:cursor-not-allowed disabled:opacity-55'
|
||||
'flex w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-foreground/95',
|
||||
'transition-colors hover:bg-accent/60 disabled:cursor-not-allowed disabled:opacity-55',
|
||||
selectedChoice === choice && 'bg-accent/60'
|
||||
)}
|
||||
data-choice
|
||||
disabled={!ready || submitting}
|
||||
key={`${index}-${choice}`}
|
||||
onClick={() => void respond(choice)}
|
||||
onClick={() => {
|
||||
setSelectedChoice(choice)
|
||||
void respond(choice)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="grid size-5 shrink-0 place-items-center rounded-md bg-muted text-[0.6875rem] font-mono tabular-nums text-muted-foreground group-hover/choice:bg-background">
|
||||
{index + 1}
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'grid size-3.5 shrink-0 place-items-center rounded-full border transition-colors',
|
||||
selectedChoice === choice ? 'border-primary' : 'border-muted-foreground/40'
|
||||
)}
|
||||
>
|
||||
{selectedChoice === choice && <span className="size-1.5 rounded-full bg-primary" />}
|
||||
</span>
|
||||
<span className="flex-1 wrap-anywhere">{choice}</span>
|
||||
{selectedChoice === choice && <Check aria-hidden className="size-4 shrink-0 text-primary" />}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-lg border border-dashed border-border/60 bg-transparent px-3 py-2 text-left text-sm text-muted-foreground',
|
||||
'transition-colors hover:border-border hover:bg-accent/40 hover:text-foreground'
|
||||
'flex w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-muted-foreground',
|
||||
'transition-colors hover:bg-accent/40 hover:text-foreground'
|
||||
)}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
@@ -213,12 +202,7 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="grid size-5 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground"
|
||||
>
|
||||
<PencilLine className="size-3" />
|
||||
</span>
|
||||
<span aria-hidden className="size-3.5 shrink-0 rounded-full border border-muted-foreground/40" />
|
||||
<span className="flex-1">Other (type your answer)</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -227,7 +211,7 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
|
||||
{(typing || !hasChoices) && (
|
||||
<form className="grid gap-2" onSubmit={handleSubmitFreeform}>
|
||||
<Textarea
|
||||
className="min-h-20 resize-y rounded-lg border-border/70 bg-background/60 text-sm"
|
||||
className="min-h-20 resize-y rounded-lg border-transparent bg-accent/40 text-sm focus-visible:bg-background/60"
|
||||
disabled={submitting}
|
||||
onChange={event => setDraft(event.target.value)}
|
||||
onKeyDown={handleTextareaKey}
|
||||
@@ -270,10 +254,9 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
|
||||
)}
|
||||
|
||||
{!typing && hasChoices && (
|
||||
<div className="flex items-center justify-between text-[0.6875rem] text-muted-foreground/85">
|
||||
<span>1–{choices.length} to pick</span>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="bg-transparent text-muted-foreground/85 underline-offset-4 decoration-current/20 hover:text-foreground hover:underline disabled:opacity-50"
|
||||
className="cursor-pointer bg-transparent text-[0.6875rem] text-muted-foreground/70 underline-offset-4 hover:text-foreground hover:underline disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={!ready || submitting}
|
||||
onClick={() => void respond('')}
|
||||
type="button"
|
||||
|
||||
@@ -23,10 +23,11 @@ const buttonVariants = cva(
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10'
|
||||
icon: 'size-9 rounded-[4px]',
|
||||
'icon-xs': "size-6 rounded-[4px] [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm': 'size-8 rounded-[4px]',
|
||||
'icon-lg': 'size-10 rounded-[4px]',
|
||||
'icon-titlebar': 'h-(--titlebar-control-height) w-(--titlebar-control-size) rounded-[4px] [&_.codicon]:text-[0.875rem]'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
Reference in New Issue
Block a user