diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx
index 8146c4603b..ea660f7af8 100644
--- a/studio/frontend/src/components/assistant-ui/thread.tsx
+++ b/studio/frontend/src/components/assistant-ui/thread.tsx
@@ -64,7 +64,7 @@ import { NewProjectDialog } from "@/features/chat/components/new-project-dialog"
import { parseExternalModelId } from "@/features/chat/external-providers";
import { McpComposerButton } from "@/features/chat/mcp-composer-button";
import { getExternalReasoningCapabilities } from "@/features/chat/provider-capabilities";
-import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available";
+import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled";
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
import { useExternalProvidersStore } from "@/features/chat/stores/external-providers-store";
import { deleteThreadMessage } from "@/features/chat/utils/delete-thread-message";
@@ -876,12 +876,11 @@ const Composer: FC<{
const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled);
const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat);
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
- const ragToolAvailable = useRagToolAvailable();
// More than 4 pills: collapse to icons only. Search and Code always show;
- // Images, Canvas and MCP are conditional.
+ // Images, RAG, Canvas and MCP are conditional.
const pillsCompact =
2 +
- (ragEnabled && ragToolAvailable ? 1 : 0) +
+ (ragEnabled ? 1 : 0) +
(supportsBuiltinImageGeneration ? 1 : 0) +
(artifactsEnabled ? 1 : 0) +
(mcpEnabledForChat ? 1 : 0) >
@@ -2017,8 +2016,8 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
);
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
const setRagEnabled = useChatRuntimeStore((s) => s.setRagEnabled);
- // Shared gate so the menu row agrees with the RAG pill and Add Files bar.
- const ragAvailable = useRagToolAvailable();
+ // Shared gate so the menu row agrees with the RAG pill.
+ const ragDisabled = useRagToolDisabled();
// Capability gating mirrors the visible pills so menu and pills agree on
// what a loaded model supports (a tool the backend drops must not look on).
const modelLoaded = useChatRuntimeStore(
@@ -2180,10 +2179,11 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
}
onSelect={() => setCodeToolsEnabled(!codeToolsEnabled)}
>
+ {/* Scale, not width: an oversized box pushed the label out of line. */}
Code
{codeToolsEnabled && !codeDisabled ? (
@@ -2217,15 +2217,15 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
)}
setRagEnabled(!ragEnabled)}
>
- RAG
- {ragEnabled && ragAvailable ? (
+ Chat with Files
+ {ragEnabled && !ragDisabled ? (
= ({
/>
) : null}
- {/* Top-level so it stays one click away (not buried in More). */}
- startCompare()}>
-
- Compare chat
-
-
-
-
- Saved prompts
-
-
- {recentPrompts.map((p) => (
- aui.composer().setText(p.text)}
- >
- {p.name}
-
- ))}
- {recentPrompts.length > 0 ? : null}
- setPromptStorageOpen(true)}>
- All saved prompts…
-
-
-
- {/* Top-level: a third-level submenu collision-flips at narrow widths
- and is awkward to reach. */}
-
-
-
- Export chat
-
-
- {
- if (!activeThreadId) return;
- exportConversationRawJsonl(activeThreadId).catch(() =>
- toast.error("Export failed."),
- );
- }}
- >
- Raw JSONL
-
- {
- if (!activeThreadId) return;
- exportConversationCsv(activeThreadId).catch(() =>
- toast.error("Export failed."),
- );
- }}
- >
- CSV
-
- {
- if (!activeThreadId) return;
- exportConversationShareGPT(activeThreadId).catch(() =>
- toast.error("Export failed."),
- );
- }}
- >
- ShareGPT JSONL
-
-
-
More
+ startCompare()}>
+
+ Compare chat
+
+
+
+
+ Saved prompts
+
+
+ {recentPrompts.map((p) => (
+ aui.composer().setText(p.text)}
+ >
+ {p.name}
+
+ ))}
+ {recentPrompts.length > 0 ? : null}
+ setPromptStorageOpen(true)}>
+ All saved prompts…
+
+
+
+
+
+
+ Export chat
+
+
+ {
+ if (!activeThreadId) return;
+ exportConversationRawJsonl(activeThreadId).catch(() =>
+ toast.error("Export failed."),
+ );
+ }}
+ >
+ Raw JSONL
+
+ {
+ if (!activeThreadId) return;
+ exportConversationCsv(activeThreadId).catch(() =>
+ toast.error("Export failed."),
+ );
+ }}
+ >
+ CSV
+
+ {
+ if (!activeThreadId) return;
+ exportConversationShareGPT(activeThreadId).catch(() =>
+ toast.error("Export failed."),
+ );
+ }}
+ >
+ ShareGPT JSONL
+
+
+
!!s.params.checkpoint && !s.modelLoading,
);
const checkpoint = useChatRuntimeStore((s) => s.params.checkpoint);
const supportsTools = useChatRuntimeStore((s) => s.supportsTools);
return (
- modelLoaded && parseExternalModelId(checkpoint) === null && supportsTools
+ modelLoaded && (parseExternalModelId(checkpoint) !== null || !supportsTools)
);
}
diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx
index f68ec6cc73..fe71adf4d7 100644
--- a/studio/frontend/src/features/chat/shared-composer.tsx
+++ b/studio/frontend/src/features/chat/shared-composer.tsx
@@ -656,8 +656,10 @@ export function SharedComposer({
// Fetch pill: Anthropic-only (web_fetch_20250910 / web_fetch_20260209).
const webFetchDisabled = !modelLoaded || !supportsBuiltinWebFetch;
const showWebFetchPill = supportsBuiltinWebFetch;
- // Docs (RAG) pill is local-only: search_knowledge_base needs the local runtime.
- const ragDisabled = !modelLoaded || isExternalModel || !supportsTools;
+ // Docs (RAG) is local-only: search_knowledge_base needs the local runtime.
+ // Disable only when a loaded model can't run it; with no model the toggle
+ // can still be pre-selected, matching Web search/Code/MCP.
+ const ragDisabled = modelLoaded && (isExternalModel || !supportsTools);
const showRagPill = !isExternalModel;
// Above 4 pills, collapse to icons only to cut clutter. Compare, Search and
// Code always show; the rest are conditional.
@@ -1323,10 +1325,12 @@ export function SharedComposer({
}
onSelect={() => setCodeToolsEnabled(!codeToolsEnabled)}
>
+ {/* Scale, not width: an oversized box pushed the label out of
+ line. */}
Code
{codeToolsEnabled && !codeDisabled ? (
@@ -1369,7 +1373,7 @@ export function SharedComposer({
onSelect={() => setRagEnabled(!ragEnabled)}
>
- RAG
+ Chat with Files
{ragEnabled && !ragDisabled ? (
{/* RAG hidden temporarily */}
- {/* Always active: this menu only renders in compare mode. Ticked
- like Web search/Code; click toggles it off. */}
-
-
- Compare chat
-
-
-
-
-
- Saved prompts
-
-
- {recentPrompts.map((p) => (
- {
- setText(p.text);
- requestAnimationFrame(() =>
- textareaRef.current?.focus(),
- );
- }}
- >
- {p.name}
-
- ))}
- {recentPrompts.length > 0 ? : null}
- setPromptStorageOpen(true)}>
- All saved prompts…
-
-
-
- {/* Top-level: a third-level submenu collision-flips at narrow
- widths and is awkward to reach. */}
-
-
-
- Export chat
-
-
- {[
- { label: "Raw JSONL", fn: exportConversationRawJsonl },
- { label: "CSV", fn: exportConversationCsv },
- {
- label: "ShareGPT JSONL",
- fn: exportConversationShareGPT,
- },
- ].map(({ label, fn }) => (
- {
- if (!exportThreadIds.length) {
- toast.error("No conversation to export yet.");
- return;
- }
- Promise.all(exportThreadIds.map((id) => fn(id))).catch(
- () => toast.error("Export failed."),
- );
- }}
- >
- {label}
-
- ))}
-
-
More
+ {/* Always active: this menu only renders in compare mode. Ticked
+ like Web search/Code; click toggles it off. */}
+
+
+ Compare chat
+
+
+
+
+
+ Saved prompts
+
+
+ {recentPrompts.map((p) => (
+ {
+ setText(p.text);
+ requestAnimationFrame(() =>
+ textareaRef.current?.focus(),
+ );
+ }}
+ >
+ {p.name}
+
+ ))}
+ {recentPrompts.length > 0 ? (
+
+ ) : null}
+ setPromptStorageOpen(true)}
+ >
+ All saved prompts…
+
+
+
+
+
+
+ Export chat
+
+
+ {[
+ { label: "Raw JSONL", fn: exportConversationRawJsonl },
+ { label: "CSV", fn: exportConversationCsv },
+ {
+ label: "ShareGPT JSONL",
+ fn: exportConversationShareGPT,
+ },
+ ].map(({ label, fn }) => (
+ {
+ if (!exportThreadIds.length) {
+ toast.error("No conversation to export yet.");
+ return;
+ }
+ Promise.all(
+ exportThreadIds.map((id) => fn(id)),
+ ).catch(() => toast.error("Export failed."));
+ }}
+ >
+ {label}
+
+ ))}
+
+
void;
setMcpEnabledForChat: (enabled: boolean) => void;
setWebFetchToolsEnabled: (enabled: boolean) => void;
- setRagEnabled: (enabled: boolean, options?: { persist?: boolean }) => void;
+ setRagEnabled: (enabled: boolean) => void;
setRagSource: (source: RagSource) => void;
setRagMode: (mode: RagMode) => void;
setRagTopK: (topK: number) => void;
@@ -746,7 +745,8 @@ export const useChatRuntimeStore = create((set, get) => ({
),
mcpEnabledForChat: loadBool(CHAT_MCP_ENABLED_KEY, false),
webFetchToolsEnabled: loadBool(CHAT_WEB_FETCH_TOOLS_ENABLED_KEY, false),
- ragEnabled: loadBool(CHAT_RAG_ENABLED_KEY, false),
+ // RAG is opt-in per session: always starts off, never restored from storage.
+ ragEnabled: false,
ragSource: loadRagSource(),
ragMode: loadRagMode(),
ragTopK: loadRagTopK(),
@@ -1072,13 +1072,7 @@ export const useChatRuntimeStore = create((set, get) => ({
saveBool(CHAT_WEB_FETCH_TOOLS_ENABLED_KEY, webFetchToolsEnabled);
return { webFetchToolsEnabled };
}),
- setRagEnabled: (ragEnabled, options) =>
- set(() => {
- if (options?.persist !== false) {
- saveBool(CHAT_RAG_ENABLED_KEY, ragEnabled);
- }
- return { ragEnabled };
- }),
+ setRagEnabled: (ragEnabled) => set(() => ({ ragEnabled })),
setRagSource: (ragSource) =>
set(() => {
saveRagSource(ragSource);
diff --git a/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx b/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx
index b703fca347..e040d1842a 100644
--- a/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx
+++ b/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx
@@ -14,7 +14,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available";
+import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled";
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
import { listKnowledgeBases } from "../api/rag-api";
@@ -38,8 +38,8 @@ const ArrowDownStandardIcon: FC<{ className?: string }> = ({ className }) => (
);
-// Picks the retrieval source. Only rendered when retrieval is on and the loaded
-// model can run search_knowledge_base.
+// Picks the retrieval source. Shown whenever retrieval is on; dims but stays
+// interactive (so it can be turned off) while the loaded model can't run it.
export function KnowledgeBaseComposerButton({
side = "bottom",
}: {
@@ -47,7 +47,7 @@ export function KnowledgeBaseComposerButton({
} = {}) {
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
const setRagEnabled = useChatRuntimeStore((s) => s.setRagEnabled);
- const ragAvailable = useRagToolAvailable();
+ const ragDisabled = useRagToolDisabled();
const ragSource = useChatRuntimeStore((s) => s.ragSource);
const setRagSource = useChatRuntimeStore((s) => s.setRagSource);
@@ -85,7 +85,7 @@ export function KnowledgeBaseComposerButton({
}
}, [kbs, kbsLoaded, ragSource, setRagSource]);
- if (!ragEnabled || !ragAvailable) return null;
+ if (!ragEnabled) return null;
return (
<>
@@ -100,7 +100,7 @@ export function KnowledgeBaseComposerButton({