Skip to content

Commit d4afe62

Browse files
dovvnloadingclaude
andauthored
R8a: stabilize composer control width, explain the Reasoning tooltip (findings #7, #11) (#179)
Two composer-control issues from the audit, same component. - Finding #7: .control-value used max-width (a cap, not a fixed size), so switching reasoning levels (or the active model) resized the button itself, visibly shoving the adjacent control sideways. Changed to a fixed width - both the Reasoning and Model controls now stay one stable size regardless of which option is selected; long model ids still ellipsis- truncate, they just no longer resize the button while doing it. - Finding #11: the Reasoning button had no title at all, unlike its Attach and Model neighbours, which both explain their disabled state. Added a conditional title distinguishing "provider doesn't support this" from "a request is in flight" - the same two reasons the button can be disabled for. Live-verified against a running instance: the Reasoning control's rendered width stayed at 238.5625px across all four levels (Off/Low/Medium/High). Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent cae7bd1 commit d4afe62

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

‎web_ui/src/app/chrome/Composer.test.tsx‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,52 @@ describe("Composer", () => {
344344
expect(container.querySelector('[data-overlay-trigger="reasoning"]')).not.toBeDisabled();
345345
});
346346

347+
it("R8a finding #11: Reasoning trigger explains itself via title when disabled for provider reasons", () => {
348+
const { store } = makeStore({
349+
composer: {
350+
capabilities: { ...initialComposerState.capabilities, reasoningSelection: false },
351+
request: { ...initialComposerState.request, canSend: true },
352+
},
353+
});
354+
const { container } = render(
355+
<OverlayProvider>
356+
{/* @ts-expect-error - test double */}
357+
<Composer store={store} sceneStore={makeSceneStore().sceneStore} />
358+
</OverlayProvider>,
359+
);
360+
expect(container.querySelector('[data-overlay-trigger="reasoning"]')).toHaveAttribute(
361+
"title",
362+
"Reasoning mode is only available for local Ollama and Llama.cpp providers",
363+
);
364+
});
365+
366+
it("R8a finding #11: Reasoning trigger explains itself via title when disabled because a request is busy", () => {
367+
const { store } = makeStore();
368+
const { container } = render(
369+
<OverlayProvider>
370+
{/* @ts-expect-error - test double */}
371+
<Composer store={store} sceneStore={makeSceneStore().sceneStore} />
372+
</OverlayProvider>,
373+
);
374+
expect(container.querySelector('[data-overlay-trigger="reasoning"]')).toHaveAttribute(
375+
"title",
376+
"Wait for the current response to finish",
377+
);
378+
});
379+
380+
it("R8a finding #11: Reasoning trigger has no title at all when it is enabled - nothing to explain", () => {
381+
const { store } = makeStore({
382+
composer: { request: { ...initialComposerState.request, canSend: true } },
383+
});
384+
const { container } = render(
385+
<OverlayProvider>
386+
{/* @ts-expect-error - test double */}
387+
<Composer store={store} sceneStore={makeSceneStore().sceneStore} />
388+
</OverlayProvider>,
389+
);
390+
expect(container.querySelector('[data-overlay-trigger="reasoning"]')).not.toHaveAttribute("title");
391+
});
392+
347393
it("the Attach button is disabled when capabilities.attachments is false, even with canSend true", () => {
348394
const { store } = makeStore({
349395
composer: {

‎web_ui/src/app/chrome/Composer.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ export function Composer({ store, sceneStore }: { store: ComposerStore; sceneSto
143143
aria-haspopup="dialog"
144144
aria-pressed={overlays.isOpen("reasoning")}
145145
disabled={!composer.capabilities.reasoningSelection || !composer.request.canSend}
146+
/* R8a (UI/UX issue list finding #11): its two disabled-reason
147+
neighbours (Attach, Model) both explain themselves via title -
148+
this button had none at all, so a greyed-out Reasoning chip in
149+
API mode looked like just another broken control rather than a
150+
provider limitation. */
151+
title={
152+
!composer.capabilities.reasoningSelection
153+
? "Reasoning mode is only available for local Ollama and Llama.cpp providers"
154+
: !composer.request.canSend
155+
? "Wait for the current response to finish"
156+
: undefined
157+
}
146158
onClick={() => overlays.toggle("reasoning", "popover")}
147159
>
148160
<span className="control-kicker">Reasoning</span>

‎web_ui/src/app/styles.css‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1550,10 +1550,18 @@ body,
15501550
text-transform: uppercase;
15511551
}
15521552

1553+
/* R8a (UI/UX issue list finding #7): was max-width, a cap rather than a
1554+
fixed size, so a shorter label (e.g. reasoning level "Off") left the
1555+
button narrower than a longer one ("Medium"), and the adjacent control
1556+
visibly shifted sideways every time the value changed. A fixed width
1557+
keeps both the Reasoning and Model controls one stable size regardless
1558+
of which option/model is currently selected - long model ids still
1559+
ellipsis-truncate via the properties below, they just no longer resize
1560+
the button itself while doing it. */
15531561
.control-value {
15541562
font-size: 11px;
15551563
font-weight: 600;
1556-
max-width: 140px;
1564+
width: 140px;
15571565
overflow: hidden;
15581566
text-overflow: ellipsis;
15591567
white-space: nowrap;

0 commit comments

Comments
 (0)