diff --git a/src/features/agents/Composer.tsx b/src/features/agents/Composer.tsx index fbae7ff59..84e7682ec 100644 --- a/src/features/agents/Composer.tsx +++ b/src/features/agents/Composer.tsx @@ -62,6 +62,7 @@ import { useCompletionAnchor } from "./useCompletionAnchor"; import { useAgentSettings } from "./agentSettings"; import styles from "./Composer.module.css"; import { isImeKey } from "@/platform/ime"; +import { isMacHost } from "@/platform/keymap"; const MAX_PROMPT_HEIGHT = 280; const MAX_ITEMS = 12; @@ -454,6 +455,12 @@ export function Composer(props: ComposerProps) { recalled.current = { index: -1, stash: "" }; return; } + // macOS text boxes scroll on Home / End and leave the caret where it is. + if ((e.key === "Home" || e.key === "End") && !e.altKey && !e.ctrlKey && !e.metaKey && isMacHost()) { + e.preventDefault(); + getSelection()?.modify(e.shiftKey ? "extend" : "move", e.key === "Home" ? "backward" : "forward", "lineboundary"); + return; + } if (e.key === "Tab" && e.shiftKey) { const mode = modeOption(props.config); const value = mode && nextValue(mode); diff --git a/src/features/agents/composerCommands.test.tsx b/src/features/agents/composerCommands.test.tsx index 0aaf28e80..94726b8c2 100644 --- a/src/features/agents/composerCommands.test.tsx +++ b/src/features/agents/composerCommands.test.tsx @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { sendKeyAction } from "./Composer"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -43,6 +43,7 @@ vi.mock("@/platform/tauriCore", async (importOriginal) => ({ import { isTauri } from "@/platform/ipc"; import { useProject } from "@/platform/project"; +import { usePlatform } from "@/platform/platformInfo"; import type { ProjectInfo } from "@/bindings"; import { defaultTaskProject } from "./project"; import { HomeEditor } from "./HomeEditor"; @@ -243,6 +244,35 @@ describe("New Task composer commands", () => { }); }); +describe("New Task composer Home / End", () => { + const os = usePlatform.getState().os; + afterEach(() => { + usePlatform.setState({ os }); + vi.restoreAllMocks(); + }); + + it("moves the caret to the line's start or end on macOS, extending the selection with Shift", () => { + usePlatform.setState({ os: "macos" }); + const modify = vi.fn(); + vi.spyOn(window, "getSelection").mockReturnValue({ modify } as unknown as Selection); + render(); + const box = screen.getByRole("textbox"); + expect(fireEvent.keyDown(box, { key: "Home" })).toBe(false); + fireEvent.keyDown(box, { key: "End", shiftKey: true }); + fireEvent.keyDown(box, { key: "End", metaKey: true }); + expect(modify.mock.calls).toEqual([ + ["move", "backward", "lineboundary"], + ["extend", "forward", "lineboundary"], + ]); + }); + + it("leaves Home / End to the text box elsewhere", () => { + usePlatform.setState({ os: "windows" }); + render(); + expect(fireEvent.keyDown(screen.getByRole("textbox"), { key: "Home" })).toBe(true); + }); +}); + describe("New Task composer paste", () => { it("attaches the clipboard's image when WebKitGTK pastes an empty clipboardData", async () => { render();