diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx index 8d10cb8458e..a11d858b8a6 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx @@ -11,7 +11,7 @@ import { cancelToolCall } from "../../../../redux/slices/sessionSlice"; import { callToolById } from "../../../../redux/thunks/callToolById"; import { cancelStream } from "../../../../redux/thunks/cancelStream"; import { logToolUsage } from "../../../../redux/util"; -import { isJetBrains } from "../../../../util"; +import { isJetBrains, isTextInputTarget } from "../../../../util"; import { useMainEditor } from "../../TipTapEditor"; import { BlockSettingsTopToolbar } from "./BlockSettingsTopToolbar"; import { EditOutcomeToolbar } from "./EditOutcomeToolbar"; @@ -117,7 +117,7 @@ export function LumpToolbar() { // Also stop regular streaming if it's happening if (isStreaming) { - dispatch(cancelStream()); + void dispatch(cancelStream()); } }; @@ -127,6 +127,9 @@ export function LumpToolbar() { } const handleToolCallKeyboardShortcuts = (event: KeyboardEvent) => { + if (isTextInputTarget(event.target)) { + return; + } if (isExecuteToolCallShortcut(event) && firstPendingToolCall) { event.preventDefault(); event.stopPropagation(); diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index 3a79c07b83a..1c78a09eba0 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -259,15 +259,6 @@ export function createEditorConfig(options: { return true; }, - "Mod-Backspace": () => { - // If you press cmd+backspace wanting to cancel, - // but are inside of a text box, it shouldn't - // delete the text - if (isStreamingRef.current) { - return true; - } - return false; - }, "Shift-Enter": () => this.editor.commands.first(({ commands }) => [ () => commands.newlineInCode(), diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 00e04efd1f4..fe93907f09e 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -40,7 +40,11 @@ import { import { streamEditThunk } from "../../redux/thunks/edit"; import { loadLastSession } from "../../redux/thunks/session"; import { streamResponseThunk } from "../../redux/thunks/streamResponse"; -import { isJetBrains, isMetaEquivalentKeyPressed } from "../../util"; +import { + isJetBrains, + isMetaEquivalentKeyPressed, + isTextInputTarget, +} from "../../util"; import { ToolCallDiv } from "./ToolCallDiv"; import { useStore } from "react-redux"; @@ -136,6 +140,9 @@ export function Chat() { useEffect(() => { // Cmd + Backspace to delete current step const listener = (e: KeyboardEvent) => { + if (!isStreaming || isTextInputTarget(e.target)) { + return; + } if ( e.key === "Backspace" && (jetbrains ? e.altKey : isMetaEquivalentKeyPressed(e)) && diff --git a/gui/src/util/index.ts b/gui/src/util/index.ts index 81a667f6588..d72d9fc1859 100644 --- a/gui/src/util/index.ts +++ b/gui/src/util/index.ts @@ -32,6 +32,17 @@ export function isMetaEquivalentKeyPressed({ return metaKey; } } +export function isTextInputTarget(target: EventTarget | null): boolean { + if (!(target instanceof Element)) { + return false; + } + + return Boolean( + target.closest( + 'input, textarea, [contenteditable="true"], [contenteditable="plaintext-only"]', + ), + ); +} export function getMetaKeyLabel(): string { return getPlatform() === "mac" ? "⌘" : "Ctrl"; diff --git a/gui/src/util/isTextInputTarget.test.ts b/gui/src/util/isTextInputTarget.test.ts new file mode 100644 index 00000000000..4d2732a1b17 --- /dev/null +++ b/gui/src/util/isTextInputTarget.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { isTextInputTarget } from "."; + +describe("isTextInputTarget", () => { + it("matches native text-editing elements", () => { + expect(isTextInputTarget(document.createElement("input"))).toBe(true); + expect(isTextInputTarget(document.createElement("textarea"))).toBe(true); + }); + + it("matches descendants of contenteditable editors", () => { + const editor = document.createElement("div"); + editor.setAttribute("contenteditable", "true"); + const child = document.createElement("span"); + editor.appendChild(child); + + expect(isTextInputTarget(child)).toBe(true); + }); + + it("ignores non-editable targets", () => { + expect(isTextInputTarget(document.createElement("button"))).toBe(false); + expect(isTextInputTarget(null)).toBe(false); + }); +});