fix double /reset in agent mode

This commit is contained in:
shatfield4
2026-04-24 10:32:49 -07:00
parent 680d38a3ce
commit 3a362d7039
3 changed files with 62 additions and 53 deletions

View File

@@ -7,7 +7,6 @@ import AddPresetModal from "./SlashPresets/AddPresetModal";
import EditPresetModal from "./SlashPresets/EditPresetModal";
import PublishEntityModal from "@/components/CommunityHub/PublishEntityModal";
import showToast from "@/utils/toast";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import { PROMPT_INPUT_EVENT } from "@/components/WorkspaceChat/ChatContainer/PromptInput";
import useToolsMenuItems from "../../useToolsMenuItems";
import SlashCommandRow from "./SlashCommandRow";
@@ -20,7 +19,6 @@ export default function SlashCommandsTab({
registerItemCount,
}) {
const { t } = useTranslation();
const isActiveAgentSession = useIsAgentSessionActive();
const {
isOpen: isAddModalOpen,
openModal: openAddModal,
@@ -49,38 +47,31 @@ export default function SlashCommandsTab({
setPresets(presets);
};
// Build the list of selectable items for keyboard navigation and rendering
// Command names must stay as static English strings since the backend
// matches against exact "/reset" and "/exit" commands.
const items = useMemo(() => {
const builtIn = isActiveAgentSession
? {
command: "/exit",
description: t("chat_window.preset_exit_description"),
autoSubmit: true,
}
: {
command: "/reset",
description: t("chat_window.preset_reset_description"),
autoSubmit: true,
};
return [
builtIn,
// Build the list of selectable items for keyboard navigation and rendering.
// /reset is a static English string since the backend matches it exactly.
// During an agent session it ends the session AND clears the chat.
const items = useMemo(
() => [
{
command: "/reset",
description: t("chat_window.preset_reset_description"),
autoSubmit: true,
},
...presets.map((preset) => ({
command: preset.command,
description: preset.description,
autoSubmit: false,
preset,
})),
];
}, [isActiveAgentSession, presets]);
],
[presets, t]
);
const handleUseCommand = useCallback(
(command, autoSubmit = false) => {
setShowing(false);
// Auto-submit commands (/reset, /exit) fire immediately
// Auto-submit commands (/reset) fire immediately
if (autoSubmit) {
sendCommand({ text: command, autoSubmit: true });
promptRef?.current?.focus();
@@ -189,21 +180,19 @@ export default function SlashCommandsTab({
))}
{/* Add new */}
{!isActiveAgentSession && (
<div
onClick={openAddModal}
className="flex items-center gap-1.5 px-2 py-1 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100"
>
<Plus
size={12}
weight="bold"
className="text-white light:text-slate-900"
/>
<span className="text-xs text-white light:text-slate-900">
{t("chat_window.add_new")}
</span>
</div>
)}
<div
onClick={openAddModal}
className="flex items-center gap-1.5 px-2 py-1 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100"
>
<Plus
size={12}
weight="bold"
className="text-white light:text-slate-900"
/>
<span className="text-xs text-white light:text-slate-900">
{t("chat_window.add_new")}
</span>
</div>
{/* Modals */}
<AddPresetModal

View File

@@ -46,6 +46,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
const { files, parseAttachments } = useContext(DndUploaderContext);
const { chatHistoryRef } = useChatContainerQuickScroll();
const pendingMessageChecked = useRef(false);
const pendingResetRef = useRef(false);
const { listening, resetTranscript } = useSpeechRecognition({
clearTranscriptOnListen: true,
@@ -240,7 +241,13 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
attachments,
})
);
return;
// /reset during an active agent session should end the session AND
// clear the chat in a single action. The send above triggers the
// server to abort the agent and close the socket; fall through to the
// /reset flow below which resets memory + clears chat history.
if (promptMessage.userMessage.trim() !== "/reset") return;
pendingResetRef.current = true;
}
if (!promptMessage || !promptMessage?.userMessage) return false;
@@ -304,20 +311,26 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
socket.addEventListener("close", (_event) => {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
setChatHistory((prev) => [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
type: "statusResponse",
content: "Agent session complete.",
role: "assistant",
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
},
]);
// When the close was triggered by /reset, skip the "Agent session
// complete." status - the pending /reset flow will clear history.
if (pendingResetRef.current) {
pendingResetRef.current = false;
} else {
setChatHistory((prev) => [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
type: "statusResponse",
content: "Agent session complete.",
role: "assistant",
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
},
]);
}
setLoadingResponse(false);
setWebsocket(null);
setSocketId(null);

View File

@@ -12,6 +12,12 @@ const chatHistory = {
return {
name: this.name,
setup: function (aibitat) {
// If the agent is aborted (e.g. user sent /reset mid-response), skip
// the pending save so a completing in-flight response doesn't reappear.
aibitat.onAbort(() => {
aibitat._aborted = true;
});
// pre-register a workspace chat ID to secure it in the DB
aibitat.onMessage(async (message) => {
if (message.from !== "USER") return;
@@ -54,6 +60,7 @@ const chatHistory = {
aibitat.onMessage(async () => {
try {
if (aibitat._aborted) return;
const lastResponses = aibitat.chats.slice(-2);
if (lastResponses.length !== 2) return;
const [prev, last] = lastResponses;