diff --git a/content/solid/ask-card.mdx b/content/solid/ask-card.mdx index 7066f52..bf25230 100644 --- a/content/solid/ask-card.mdx +++ b/content/solid/ask-card.mdx @@ -34,4 +34,10 @@ import { AskCard } from "@/components/ui/ask-card" ## After the answer -Once the conversation has moved on, leave `onAnswer` out. The choices are then no longer offered, and the card stays as a record of what was asked. +Once the conversation has moved on, leave `onAnswer` out. The choices are then no longer offered, and the card stays as a record of what was asked. Give `chosen` the answer and it is a record of what was said too: that choice keeps its pane, the others fade to words. + +```tsx + +``` + +The choices are not disabled once answered, only inert: a disabled button drops the keyboard to the page on the very press that answers. diff --git a/content/solid/composer.mdx b/content/solid/composer.mdx index c731494..18976a2 100644 --- a/content/solid/composer.mdx +++ b/content/solid/composer.mdx @@ -50,6 +50,8 @@ continue to apply to the outer wrapper. The send mark has no block behind it in any state: its brightness says what it will do. With `running`, it is the quiet square that stops the turn, until something is typed, which is then sent behind it. +The mark is never disabled. With nothing to send and nothing to stop it is dim and does nothing, but it stays where it is, so the keyboard on it is not dropped to the page as the text goes or the turn ends. Sent or stopped from the keyboard, the focus returns to the text; the pointer never takes it away. While the text is being written, the pane's hairline is drawn in the focus line. + ## Footer diff --git a/content/solid/tag.mdx b/content/solid/tag.mdx index 63ba06f..a414e5f 100644 --- a/content/solid/tag.mdx +++ b/content/solid/tag.mdx @@ -34,3 +34,7 @@ import { Tag } from "@/components/ui/tag" ## States `progress` between 0 and 1 fills the tag from the left. `error` turns it to the colour of failure and says why on hover. `armed` is the same pane lit a step brighter, for the tag the next Backspace will remove. + +## Removed + +A tag removed on its own x hands the keyboard on before it goes: to the next tag's x, else the one before, else the text of the composer it is in. Nothing is dropped to the page. diff --git a/content/solid/tool-steps.mdx b/content/solid/tool-steps.mdx index 28f4125..d8be1d8 100644 --- a/content/solid/tool-steps.mdx +++ b/content/solid/tool-steps.mdx @@ -43,6 +43,8 @@ const steps: Step[] = [ `StepList` is the opened list on its own. A row opens to the call itself, or to its diff when it changed a file. +A group keeps the state of its rows. One call is one line; when a second joins it the group forms round the first, and if that call was open the group opens with it, with the keyboard still on the row it was on. + ## API Reference diff --git a/content/solid/turn-status.mdx b/content/solid/turn-status.mdx index 5b01dc7..7064533 100644 --- a/content/solid/turn-status.mdx +++ b/content/solid/turn-status.mdx @@ -41,7 +41,7 @@ import { TurnStatus } from "@/components/ui/turn-status" ## With steps -Given children, the line is a button and opens to show them: the calls of the step in hand. +Given children, the line is a button and opens to show them: the calls of the step in hand. Once it has had children it stays a button, with nothing to open between steps, so the keyboard on it is not dropped as a step ends. The activity label is announced by a separate status region, so the button keeps its native role and updates to the clock and token count do not repeat the announcement. When `label` is omitted or blank, the activity name supplies the announcement and diff --git a/src/components/ui/ask-card.tsx b/src/components/ui/ask-card.tsx index 2f63447..e281c23 100644 --- a/src/components/ui/ask-card.tsx +++ b/src/components/ui/ask-card.tsx @@ -6,38 +6,53 @@ import { cn } from "@/lib/utils" * A decision the agent has put to the user: the question, and its choices as * buttons when it gave any. Once the conversation has moved on, leave * `onAnswer` out: the choices are then no longer offered, and the card stays - * as a record of what was asked. + * as a record of what was asked — and, given `chosen`, of what was said: that + * choice keeps its pane, the others fade to words. */ function AskCard({ question, options = [], onAnswer, + chosen, eyebrow = "Needs your decision", className, }: { question: string options?: string[] onAnswer?: (option: string) => void + /** The option that was taken, once one was. */ + chosen?: string eyebrow?: string className?: string }) { + const answered = !onAnswer return (

{eyebrow}

{question}

{options.length > 0 && (
- {options.map((option) => ( - - ))} + {options.map((option) => { + const taken = option === chosen + return ( + // Not disabled once answered: that drops the keyboard to the page + // on the very press that answers. Inert, and still reachable. + + ) + })}
)}
diff --git a/src/components/ui/composer.tsx b/src/components/ui/composer.tsx index 2cd5528..1f3b3bc 100644 --- a/src/components/ui/composer.tsx +++ b/src/components/ui/composer.tsx @@ -70,6 +70,11 @@ function Composer({ // which is then queued behind the turn like any other message. const stops = running && !ready const Icon = stops ? Square : ArrowUp + // With nothing to send and nothing to stop, the mark does nothing. It is + // not disabled for that: a disabled button drops the keyboard to the page + // at the moment the text is sent or the turn ends, which is when the + // keyboard was last on it. + const inert = !stops && !ready const resize = React.useCallback(() => { const el = textarea.current @@ -102,7 +107,9 @@ function Composer({
event.preventDefault()} + onClick={() => { + if (inert) return + if (stops) onStop?.() + else submit() + // Sent or stopped from the keyboard, the next thing to do is write. + textarea.current?.focus() + }} className={cn( - "shrink-0 rounded-md bg-transparent transition-colors hover:bg-accent", + "shrink-0 rounded-md bg-transparent transition-colors hover:bg-accent aria-disabled:cursor-default aria-disabled:active:translate-y-0", stops ? "text-muted-foreground hover:text-foreground" : ready ? "text-foreground" - : "text-muted-foreground/50 hover:bg-transparent disabled:opacity-100" + : "text-muted-foreground/50 hover:bg-transparent" )} > @@ -217,6 +232,7 @@ function ComposerPick({ return ( diff --git a/src/components/ui/context-ring.tsx b/src/components/ui/context-ring.tsx index 300c66f..ac55043 100644 --- a/src/components/ui/context-ring.tsx +++ b/src/components/ui/context-ring.tsx @@ -79,6 +79,7 @@ function ContextRing({ - {open && } + {open && }
) } diff --git a/src/components/ui/turn-status.tsx b/src/components/ui/turn-status.tsx index 89773da..9744ca4 100644 --- a/src/components/ui/turn-status.tsx +++ b/src/components/ui/turn-status.tsx @@ -62,7 +62,13 @@ function TurnStatus({ }) { const [open, setOpen] = React.useState(false) const elapsed = useElapsed(since) - const opens = Boolean(children) + // Once the line has had something to open it stays a button. Turned back + // into plain words between steps, it would drop the keyboard on it to the + // page; between steps it is a button with nothing to open, and still there. + const [hadChildren, setHadChildren] = React.useState(Boolean(children)) + if (children && !hadChildren) setHadChildren(true) + const opens = Boolean(children) || hadChildren + const openable = Boolean(children) // The glyph is decorative. Even without visible words, expose the activity // and keep the disclosure's name independent of the clock and token count. const activityLabel = label?.trim() || activity @@ -93,12 +99,16 @@ function TurnStatus({ ) : (
diff --git a/src/examples/ask-card-demo.tsx b/src/examples/ask-card-demo.tsx index 893e2b0..fb14be6 100644 --- a/src/examples/ask-card-demo.tsx +++ b/src/examples/ask-card-demo.tsx @@ -12,7 +12,12 @@ export default function AskCardDemo() { return (
{/* Once answered, onAnswer is left out: the choices stay as a record. */} - + {answer && (

Answered “{answer}”.{" "} diff --git a/src/examples/tool-steps-demo.tsx b/src/examples/tool-steps-demo.tsx index 1ff4d00..425fe7d 100644 --- a/src/examples/tool-steps-demo.tsx +++ b/src/examples/tool-steps-demo.tsx @@ -22,14 +22,22 @@ const steps: Step[] = [ export default function ToolStepsDemo() { const [status, setStatus] = React.useState("running") + // A second call joining the first: the one line becomes a group round it. + const [joined, setJoined] = React.useState(false) const timer = React.useRef(0) React.useEffect(() => () => window.clearTimeout(timer.current), []) const finish = (next: "done" | "error") => { window.clearTimeout(timer.current) setStatus("running") + setJoined(false) timer.current = window.setTimeout(() => setStatus(next), 2000) } + const join = () => { + window.clearTimeout(timer.current) + setJoined(false) + timer.current = window.setTimeout(() => setJoined(true), 2000) + } const single: Step = { id: "single-run", label: status === "running" ? "Running python research.py" : "Ran python research.py", @@ -45,11 +53,18 @@ export default function ToolStepsDemo() {

Schedule a result, then open the call while it runs.

-
+
+
- +
) diff --git a/src/solid-glass.css b/src/solid-glass.css index 2511839..0cd3ccc 100644 --- a/src/solid-glass.css +++ b/src/solid-glass.css @@ -861,6 +861,28 @@ --tw-ring-shadow: 0 0 0 1px var(--background), 0 0 0 3px var(--focus-line); } +/* The lines of text that are buttons: a turn's status line, a step, a + thought's heading, a tag's x, the choices on a card, a pick under the + composer. They are neither fields nor panes, so a ring would be a smudge + round a word, and a change of grey alone cannot be seen (it was tried). The + one the keyboard is on gets a hairline a little off the words, in the line + the ring is drawn in. Outside the layers, so a component's outline-none + cannot take it away. */ +:is( + [data-slot="turn-status"], + [data-slot="step-row"], + [data-slot="step-group"], + [data-slot="ask-card"], + [data-slot="tag"] + ) + :is(button, summary):focus-visible, +[data-slot="reasoning"] > summary:focus-visible, +[data-slot="context-ring-detail"]:focus-visible, +[data-slot="composer-pick"]:focus-visible { + outline: 1px solid var(--focus-line); + outline-offset: 2px; +} + /* Some of shadcn/ui's menus are painted in a see-through popover colour. Here what floats is the thickest glass, whichever component it belongs to. */ [data-slot="select-content"],