fix: always show chat tool icons (#4525)

* fix: always show chat tool icons, gray out when model doesn't support them

Tool icons (Think, Search, Code) were hidden unless a model was loaded
and supported those features. Now they're always visible so users can
see and pre-select them. If a loaded model doesn't support a feature,
the button gets grayed out and disabled instead of being removed.

* refactor: centralize Qwen thinking params in store

* fix: disable tool buttons when no model is loaded

Change disabled condition from `modelLoaded && !supportsX` to
`!modelLoaded || !supportsX` so buttons are grayed out both when
no model is loaded and when the loaded model lacks the capability.

* Fix Qwen3 param clobbering and restore SuggestionItem capability guards

- Revert setReasoningEnabled() in the store to a pure boolean setter.
  Moving the Qwen3 param logic into it caused reconnect/load/refresh
  paths (which also call setReasoningEnabled) to silently overwrite
  user-customized or server-provided temperature/topP/topK/minP.

- Restore applyQwenThinkingParams() as a standalone function called
  only from explicit user toggle click handlers in thread.tsx and
  shared-composer.tsx, matching the pre-PR behavior.

- Re-add supportsReasoning/supportsTools guards in the SuggestionItem
  click handler so that clicking a suggestion card only activates
  tool toggles the loaded model actually supports.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
Wasim Yousef Said 2026-03-24 11:26:56 +01:00 committed by GitHub
commit 3dc212e218
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 84 deletions

View file

@ -132,13 +132,7 @@ const SuggestionItem: FC = () => {
const prompt = useAuiState(({ suggestion }) => suggestion.prompt);
const isDisabled = useAuiState(({ thread }) => thread.isDisabled);
const isRunning = useAuiState(({ thread }) => thread.isRunning);
const supportsTools = useChatRuntimeStore((s) => s.supportsTools);
const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning);
const allTools = SUGGESTION_TOOLS[prompt] ?? [];
const tools = allTools.filter((tool) => {
if (tool === "thinking") return supportsReasoning;
return supportsTools;
});
const tools = SUGGESTION_TOOLS[prompt] ?? [];
return (
<button
@ -335,9 +329,6 @@ function applyQwenThinkingParams(thinkingOn: boolean): void {
const store = useChatRuntimeStore.getState();
const checkpoint = store.params.checkpoint?.toLowerCase() ?? "";
if (!checkpoint.includes("qwen3")) return;
// Qwen3 & Qwen3.5 share the same recommended settings:
// Thinking ON (general): temp=1.0, top_p=0.95, top_k=20
// Thinking OFF (general): temp=0.7, top_p=0.8, top_k=20
const params = thinkingOn
? { temperature: 0.6, topP: 0.95, topK: 20, minP: 0.0 }
: { temperature: 0.7, topP: 0.8, topK: 20, minP: 0.0 };
@ -345,15 +336,18 @@ function applyQwenThinkingParams(thinkingOn: boolean): void {
}
const ReasoningToggle: FC = () => {
const modelLoaded = useChatRuntimeStore(
(s) => !!s.params.checkpoint && !s.modelLoading,
);
const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning);
const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled);
const setReasoningEnabled = useChatRuntimeStore((s) => s.setReasoningEnabled);
if (!supportsReasoning) return null;
const disabled = !modelLoaded || !supportsReasoning;
return (
<button
type="button"
disabled={disabled}
onClick={() => {
const next = !reasoningEnabled;
setReasoningEnabled(next);
@ -361,13 +355,15 @@ const ReasoningToggle: FC = () => {
}}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
reasoningEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
disabled
? "cursor-not-allowed opacity-40"
: reasoningEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
)}
aria-label={reasoningEnabled ? "Disable thinking" : "Enable thinking"}
>
{reasoningEnabled ? (
{reasoningEnabled && !disabled ? (
<LightbulbIcon className="size-3.5" />
) : (
<LightbulbOffIcon className="size-3.5" />
@ -378,21 +374,26 @@ const ReasoningToggle: FC = () => {
};
const WebSearchToggle: FC = () => {
const modelLoaded = useChatRuntimeStore(
(s) => !!s.params.checkpoint && !s.modelLoading,
);
const supportsTools = useChatRuntimeStore((s) => s.supportsTools);
const toolsEnabled = useChatRuntimeStore((s) => s.toolsEnabled);
const setToolsEnabled = useChatRuntimeStore((s) => s.setToolsEnabled);
if (!supportsTools) return null;
const disabled = !modelLoaded || !supportsTools;
return (
<button
type="button"
disabled={disabled}
onClick={() => setToolsEnabled(!toolsEnabled)}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
toolsEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
disabled
? "cursor-not-allowed opacity-40"
: toolsEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
)}
aria-label={toolsEnabled ? "Disable web search" : "Enable web search"}
>
@ -403,23 +404,28 @@ const WebSearchToggle: FC = () => {
};
const CodeToolsToggle: FC = () => {
const modelLoaded = useChatRuntimeStore(
(s) => !!s.params.checkpoint && !s.modelLoading,
);
const supportsTools = useChatRuntimeStore((s) => s.supportsTools);
const codeToolsEnabled = useChatRuntimeStore((s) => s.codeToolsEnabled);
const setCodeToolsEnabled = useChatRuntimeStore(
(s) => s.setCodeToolsEnabled,
);
if (!supportsTools) return null;
const disabled = !modelLoaded || !supportsTools;
return (
<button
type="button"
disabled={disabled}
onClick={() => setCodeToolsEnabled(!codeToolsEnabled)}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
codeToolsEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
disabled
? "cursor-not-allowed opacity-40"
: codeToolsEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
)}
aria-label={codeToolsEnabled ? "Disable code execution" : "Enable code execution"}
>

View file

@ -237,6 +237,9 @@ export function SharedComposer({
const checkpoint = s.params.checkpoint;
return s.models.find((m) => m.id === checkpoint);
});
const modelLoaded = useChatRuntimeStore(
(s) => !!s.params.checkpoint && !s.modelLoading,
);
const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning);
const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled);
const setReasoningEnabled = useChatRuntimeStore((s) => s.setReasoningEnabled);
@ -245,6 +248,8 @@ export function SharedComposer({
const setToolsEnabled = useChatRuntimeStore((s) => s.setToolsEnabled);
const codeToolsEnabled = useChatRuntimeStore((s) => s.codeToolsEnabled);
const setCodeToolsEnabled = useChatRuntimeStore((s) => s.setCodeToolsEnabled);
const reasoningDisabled = !modelLoaded || !supportsReasoning;
const toolsDisabled = !modelLoaded || !supportsTools;
const setPendingAudioStore = useChatRuntimeStore((s) => s.setPendingAudio);
const clearPendingAudioStore = useChatRuntimeStore((s) => s.clearPendingAudio);
@ -519,70 +524,73 @@ export function SharedComposer({
</TooltipIconButton>
</>
)}
{supportsReasoning && (
<button
type="button"
onClick={() => {
const next = !reasoningEnabled;
setReasoningEnabled(next);
// Qwen3/3.5: adjust params for thinking on/off
const store = useChatRuntimeStore.getState();
const cp = store.params.checkpoint?.toLowerCase() ?? "";
if (cp.includes("qwen3")) {
const p = next
? { temperature: 0.6, topP: 0.95, topK: 20, minP: 0.0 }
: { temperature: 0.7, topP: 0.8, topK: 20, minP: 0.0 };
store.setParams({ ...store.params, ...p });
}
}}
className={cn(
"flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors",
reasoningEnabled
<button
type="button"
disabled={reasoningDisabled}
onClick={() => {
const next = !reasoningEnabled;
setReasoningEnabled(next);
// Qwen3/3.5: adjust params for thinking on/off
const store = useChatRuntimeStore.getState();
const cp = store.params.checkpoint?.toLowerCase() ?? "";
if (cp.includes("qwen3")) {
const p = next
? { temperature: 0.6, topP: 0.95, topK: 20, minP: 0.0 }
: { temperature: 0.7, topP: 0.8, topK: 20, minP: 0.0 };
store.setParams({ ...store.params, ...p });
}
}}
className={cn(
"flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors",
reasoningDisabled
? "cursor-not-allowed opacity-40"
: reasoningEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
)}
aria-label={reasoningEnabled ? "Disable thinking" : "Enable thinking"}
>
{reasoningEnabled ? (
<LightbulbIcon className="size-3" />
) : (
<LightbulbOffIcon className="size-3" />
)}
<span>Think</span>
</button>
)}
{supportsTools && (
<button
type="button"
onClick={() => setToolsEnabled(!toolsEnabled)}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
toolsEnabled
)}
aria-label={reasoningEnabled ? "Disable thinking" : "Enable thinking"}
>
{reasoningEnabled && !reasoningDisabled ? (
<LightbulbIcon className="size-3" />
) : (
<LightbulbOffIcon className="size-3" />
)}
<span>Think</span>
</button>
<button
type="button"
disabled={toolsDisabled}
onClick={() => setToolsEnabled(!toolsEnabled)}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
toolsDisabled
? "cursor-not-allowed opacity-40"
: toolsEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
)}
aria-label={toolsEnabled ? "Disable web search" : "Enable web search"}
>
<GlobeIcon className="size-3.5" />
<span>Search</span>
</button>
)}
{supportsTools && (
<button
type="button"
onClick={() => setCodeToolsEnabled(!codeToolsEnabled)}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
codeToolsEnabled
)}
aria-label={toolsEnabled ? "Disable web search" : "Enable web search"}
>
<GlobeIcon className="size-3.5" />
<span>Search</span>
</button>
<button
type="button"
disabled={toolsDisabled}
onClick={() => setCodeToolsEnabled(!codeToolsEnabled)}
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
toolsDisabled
? "cursor-not-allowed opacity-40"
: codeToolsEnabled
? "bg-primary/10 text-primary hover:bg-primary/20"
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
)}
aria-label={codeToolsEnabled ? "Disable code execution" : "Enable code execution"}
>
<TerminalIcon className="size-3.5" />
<span>Code</span>
</button>
)}
)}
aria-label={codeToolsEnabled ? "Disable code execution" : "Enable code execution"}
>
<TerminalIcon className="size-3.5" />
<span>Code</span>
</button>
</div>
<div className="flex items-center gap-1">
{dictationSupported && (