Add per-chat settings UI in web app for additional system prompt and tool checkboxes

This commit is contained in:
Agent
2026-05-24 22:04:05 +00:00
committed by James Magahern
parent 0bf0f95a67
commit 4a2493c421
11 changed files with 321 additions and 32 deletions

View File

@@ -1,5 +1,22 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Check, ChevronDown, Globe2, LoaderCircle, Menu, MessageSquare, Paperclip, Pencil, Plus, Rabbit, Search, SendHorizontal, Star, Trash2, X } from "lucide-preact";
import {
Check,
ChevronDown,
Globe2,
LoaderCircle,
Menu,
MessageSquare,
Paperclip,
Pencil,
Plus,
Rabbit,
Search,
SendHorizontal,
Settings2,
Star,
Trash2,
X,
} from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
@@ -18,6 +35,7 @@ import {
attachSearchStream,
getActiveRuns,
getChat,
listChatTools,
listModels,
getSearch,
listWorkspaceItems,
@@ -27,6 +45,7 @@ import {
updateChatTitle,
updateChatStar,
updateSearchStar,
updateChatSettings,
getMessageAttachments,
type ChatAttachment,
type ActiveRunsResponse,
@@ -34,6 +53,7 @@ import {
type Provider,
type ChatDetail,
type ChatSummary,
type ChatToolInfo,
type CompletionRequestMessage,
type Message,
type SearchDetail,
@@ -379,6 +399,30 @@ function getProviderLabel(provider: Provider | null | undefined) {
return "";
}
function getToolLabel(name: string) {
if (name === "web_search") return "Web search";
if (name === "fetch_url") return "Fetch URL";
if (name === "codex_exec") return "Codex";
if (name === "shell_exec") return "Shell";
return name
.split("_")
.filter(Boolean)
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
.join(" ");
}
function getDefaultEnabledTools(availableTools: ChatToolInfo[]) {
return availableTools.map((tool) => tool.name);
}
function normalizeEnabledTools(value: unknown, availableTools: ChatToolInfo[]) {
const available = new Set(availableTools.map((tool) => tool.name));
if (!Array.isArray(value)) return getDefaultEnabledTools(availableTools);
return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) =>
available.has(name)
);
}
function getChatModelSelection(chat: Pick<ChatSummary, "lastUsedProvider" | "lastUsedModel"> | Pick<ChatDetail, "lastUsedProvider" | "lastUsedModel"> | null) {
if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null;
return {
@@ -748,6 +792,7 @@ export default function App() {
const [isComposerDropActive, setIsComposerDropActive] = useState(false);
const [provider, setProvider] = useState<Provider>("openai");
const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
const [availableChatTools, setAvailableChatTools] = useState<ChatToolInfo[]>([]);
const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
const [model, setModel] = useState(() => {
const stored = loadStoredModelPreferences();
@@ -774,6 +819,9 @@ export default function App() {
const [renameChatDraft, setRenameChatDraft] = useState("");
const [renameChatError, setRenameChatError] = useState<string | null>(null);
const [isRenamingChat, setIsRenamingChat] = useState(false);
const [isChatSettingsOpen, setIsChatSettingsOpen] = useState(false);
const [additionalSystemPrompt, setAdditionalSystemPrompt] = useState("");
const [enabledTools, setEnabledTools] = useState<string[]>([]);
const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
const transcriptContainerRef = useRef<HTMLDivElement>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null);
@@ -899,6 +947,9 @@ export default function App() {
searchRunCountersRef.current.clear();
setComposer("");
setPendingAttachments([]);
setIsChatSettingsOpen(false);
setAdditionalSystemPrompt("");
setEnabledTools([]);
setIsQuickQuestionOpen(false);
setQuickPrompt("");
setQuickSubmittedPrompt(null);
@@ -968,6 +1019,21 @@ export default function App() {
}
};
const refreshChatTools = async () => {
try {
const tools = await listChatTools();
setAvailableChatTools(tools);
setEnabledTools((current) => normalizeEnabledTools(current.length ? current : null, tools));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes("bearer token")) {
handleAuthFailure(message);
} else {
setError(message);
}
}
};
const refreshActiveRuns = async () => {
try {
const data = await getActiveRuns();
@@ -1020,7 +1086,7 @@ export default function App() {
if (!isAuthenticated) return;
const preferredSelection = initialRouteSelectionRef.current;
initialRouteSelectionRef.current = null;
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshActiveRuns()]);
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshChatTools(), refreshActiveRuns()]);
}, [isAuthenticated]);
useEffect(() => {
@@ -1287,6 +1353,19 @@ export default function App() {
setModel(nextSelection.model);
}, [draftKind, selectedChat, selectedChatSummary, selectedItem]);
useEffect(() => {
if (draftKind === "chat") {
setAdditionalSystemPrompt("");
setEnabledTools(getDefaultEnabledTools(availableChatTools));
return;
}
if (selectedItem?.kind !== "chat") return;
const chat = selectedChat?.id === selectedItem.id ? selectedChat : selectedChatSummary;
if (!chat) return;
setAdditionalSystemPrompt(chat.additionalSystemPrompt ?? "");
setEnabledTools(normalizeEnabledTools(chat.enabledTools, availableChatTools));
}, [availableChatTools, draftKind, selectedChat, selectedChatSummary, selectedItem]);
const selectedTitle = useMemo(() => {
if (draftKind === "chat") return "New chat";
if (draftKind === "search") return "New search";
@@ -1441,6 +1520,8 @@ export default function App() {
initiatedModel: updatedChat.initiatedModel,
lastUsedProvider: updatedChat.lastUsedProvider,
lastUsedModel: updatedChat.lastUsedModel,
additionalSystemPrompt: updatedChat.additionalSystemPrompt,
enabledTools: updatedChat.enabledTools,
};
});
};
@@ -1768,6 +1849,8 @@ export default function App() {
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
additionalSystemPrompt: chat.additionalSystemPrompt,
enabledTools: chat.enabledTools,
messages: [],
});
setSelectedSearch(null);
@@ -2349,6 +2432,8 @@ export default function App() {
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
additionalSystemPrompt: chat.additionalSystemPrompt,
enabledTools: chat.enabledTools,
messages: [],
});
setSelectedSearch(null);
@@ -2527,6 +2612,8 @@ export default function App() {
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
additionalSystemPrompt: chat.additionalSystemPrompt,
enabledTools: chat.enabledTools,
messages: [],
});
setSelectedSearch(null);