From 5126ac4592d9fe1d45187a371b0b18af7469b8f5 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Sat, 22 Aug 2026 07:18:21 -0400 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E4=BC=9A=E8=AF=9D=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E5=8F=91=E9=80=81=E9=94=AE=E4=BD=8D=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.3.2 把 Ctrl+Enter 发送改成了 Enter 发送,习惯旧键位的用户没有退路。 系统设置新增「会话输入框发送键位」,两种模式二选一: - Enter 发送,Shift+Enter 换行(默认,即 0.3.2 现行为) - Ctrl+Enter 发送,Enter 换行(0.3.2 之前的键位) 实现: - 新增 lib/chat-send-mode.ts,复用已有的 local-storage.client 读写封装; 纯前端偏好,不入库、不随账号同步 - 对话页与任务详情主 Agent 会话原本各自内联了一份相同判断,统一收敛到 shouldSubmitOnKey(),加模式时不必两处各改一遍 - enter 模式保留原判断(只排除 Shift),没顺手把 Ctrl+Enter 也变成换行—— 原本它是会发送的,改掉会让没动设置的用户感到意外 - 中文输入法保护照旧:isComposing / keyCode 229 时放行,回车选词不误发送 - useSyncExternalStore + 自维护订阅集合:storage 事件只在其他标签页触发, 本页改完需手动 emit 才能立即生效;getServerSnapshot 给默认值避免 SSR 不一致 - ctrl-enter 模式同时接受 Ctrl 与 Cmd(macOS) 按需求放在系统设置页,未采用 issue 中建议的输入框内控件。 Closes #39 Claude-Session: https://claude.ai/code/session_01Q8nhYZfS4H8fsSCBJXxbSy --- web/src/app/(main)/chat/page.tsx | 6 +- .../tasks/detail/_tabs/sessions-tab.tsx | 6 +- web/src/app/(main)/system/settings/page.tsx | 41 +++++++++++- web/src/lib/chat-send-mode.ts | 65 +++++++++++++++++++ 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 web/src/lib/chat-send-mode.ts diff --git a/web/src/app/(main)/chat/page.tsx b/web/src/app/(main)/chat/page.tsx index 6c1dc66..ded6bce 100644 --- a/web/src/app/(main)/chat/page.tsx +++ b/web/src/app/(main)/chat/page.tsx @@ -47,6 +47,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/lib/api"; +import { shouldSubmitOnKey, useChatSendMode } from "@/lib/chat-send-mode"; import type { Activity, Agent, ChatAttachment, Conversation, LLMProfile } from "@/lib/types"; import { cn } from "@/lib/utils"; @@ -128,9 +129,10 @@ function Composer({ }) { const fileInputRef = React.useRef(null); const atts = attachments ?? []; + // 发送键位由系统设置决定(localStorage),默认 Enter 发送。 + const sendMode = useChatSendMode(); function onKeyDown(e: React.KeyboardEvent) { - // Enter 发送;Shift+Enter 换行。isComposing 时是中文等输入法在选字,不拦截。 - if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return; + if (!shouldSubmitOnKey(e, sendMode)) return; e.preventDefault(); onSend(); } diff --git a/web/src/app/(main)/function/tasks/detail/_tabs/sessions-tab.tsx b/web/src/app/(main)/function/tasks/detail/_tabs/sessions-tab.tsx index 0346d98..1966857 100644 --- a/web/src/app/(main)/function/tasks/detail/_tabs/sessions-tab.tsx +++ b/web/src/app/(main)/function/tasks/detail/_tabs/sessions-tab.tsx @@ -44,6 +44,7 @@ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } fro import { ScrollArea } from "@/components/ui/scroll-area"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { api, sseUrl } from "@/lib/api"; +import { shouldSubmitOnKey, useChatSendMode } from "@/lib/chat-send-mode"; import { MOCK } from "@/lib/mock/enabled"; import type { Activity, @@ -1239,6 +1240,8 @@ export function SessionsTab({ taskId }: { taskId: string }) { } const mainLoaded = !!store.main?.loaded; + // 发送键位由系统设置决定(localStorage),默认 Enter 发送。 + const sendMode = useChatSendMode(); // What the transcript pane should show for the active session. const showLoader = !activeState || (activeState.loading && !activeState.loaded); const resolutionForSession = (session: Session): TaskLLMResolution | undefined => { @@ -1511,8 +1514,7 @@ export function SessionsTab({ taskId }: { taskId: string }) { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { - if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) - return; + if (!shouldSubmitOnKey(e, sendMode)) return; e.preventDefault(); if (!mainLive) send(); }} diff --git a/web/src/app/(main)/system/settings/page.tsx b/web/src/app/(main)/system/settings/page.tsx index 92d32c0..67f53b0 100644 --- a/web/src/app/(main)/system/settings/page.tsx +++ b/web/src/app/(main)/system/settings/page.tsx @@ -2,7 +2,7 @@ import * as React from "react"; -import { CpuIcon, RadioTowerIcon, SearchIcon } from "lucide-react"; +import { CpuIcon, KeyboardIcon, RadioTowerIcon, SearchIcon } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -12,6 +12,12 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { api } from "@/lib/api"; +import { + CHAT_SEND_MODE_OPTIONS, + type ChatSendMode, + setChatSendMode, + useChatSendMode, +} from "@/lib/chat-send-mode"; import type { Settings } from "@/lib/types"; export default function SystemSettingsPage() { @@ -32,6 +38,8 @@ export default function SystemSettingsPage() { const [pyInterp, setPyInterp] = React.useState(""); const [workers, setWorkers] = React.useState("3"); const [savingWorkers, setSavingWorkers] = React.useState(false); + // 纯前端偏好:不走 /api/settings,直接读写 localStorage。 + const sendMode = useChatSendMode(); const apply = React.useCallback((s: Settings) => { setTrafficCapture(!!s.traffic_capture); @@ -424,6 +432,37 @@ export default function SystemSettingsPage() { + + + + + + 会话输入框发送键位 + + + 对话页与任务详情的主 Agent 会话输入框共用此设置,选择后立即生效、无需保存。 +
+ 该偏好只存在本浏览器,不随账号同步,换浏览器或清理站点数据后需重新设置。 +
+
+ + + + +
); } diff --git a/web/src/lib/chat-send-mode.ts b/web/src/lib/chat-send-mode.ts new file mode 100644 index 0000000..c8bcf18 --- /dev/null +++ b/web/src/lib/chat-send-mode.ts @@ -0,0 +1,65 @@ +"use client"; + +import * as React from "react"; + +import { getLocalStorageValue, setLocalStorageValue } from "@/lib/local-storage.client"; + +// 会话输入框的发送/换行键位。纯前端偏好:只落 localStorage,不入库、不随账号同步, +// 因此换浏览器需要重设。见 issue #39——0.3.2 把 Ctrl+Enter 发送改成了 Enter 发送, +// 这里把旧键位还回来作为可选项。 +export type ChatSendMode = "enter" | "ctrl-enter"; + +export const CHAT_SEND_MODE_KEY = "artex_chat_send_mode"; +export const DEFAULT_CHAT_SEND_MODE: ChatSendMode = "enter"; + +export const CHAT_SEND_MODE_OPTIONS: { value: ChatSendMode; label: string }[] = [ + { value: "enter", label: "Enter 发送,Shift+Enter 换行" }, + { value: "ctrl-enter", label: "Ctrl+Enter 发送,Enter 换行" }, +]; + +function parseMode(raw: string | null): ChatSendMode { + return raw === "ctrl-enter" || raw === "enter" ? raw : DEFAULT_CHAT_SEND_MODE; +} + +// 同一标签页内的订阅者集合。localStorage 的 storage 事件只在「其他」标签页触发, +// 本页在设置里改完后要靠 emit 通知同页的输入框,否则得刷新才生效。 +const listeners = new Set<() => void>(); + +function subscribe(listener: () => void) { + listeners.add(listener); + window.addEventListener("storage", listener); + return () => { + listeners.delete(listener); + window.removeEventListener("storage", listener); + }; +} + +// 返回的是字符串字面量,Object.is 按值比较,不会让 useSyncExternalStore 陷入循环。 +function getSnapshot(): ChatSendMode { + return parseMode(getLocalStorageValue(CHAT_SEND_MODE_KEY)); +} + +// 服务端没有 localStorage,先渲染默认值,hydrate 后 getSnapshot 再纠正。 +function getServerSnapshot(): ChatSendMode { + return DEFAULT_CHAT_SEND_MODE; +} + +export function useChatSendMode(): ChatSendMode { + return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +} + +export function setChatSendMode(mode: ChatSendMode) { + setLocalStorageValue(CHAT_SEND_MODE_KEY, mode); + for (const listener of listeners) listener(); +} + +// shouldSubmitOnKey 判断一次按键是否应当发送。 +// isComposing / keyCode 229 是中文等输入法正在选字,必须放行,否则回车选词会误发送。 +// enter 模式只排除 Shift,与 0.3.2 的行为逐字保持一致——不改设置的用户手感不变。 +// ctrl-enter 模式同时接受 Ctrl 与 Cmd(macOS)。 +export function shouldSubmitOnKey(e: React.KeyboardEvent, mode: ChatSendMode): boolean { + if (e.key !== "Enter") return false; + if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return false; + if (mode === "ctrl-enter") return e.ctrlKey || e.metaKey; + return !e.shiftKey; +}