feat(ui): 会话输入框发送键位可配置
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
This commit is contained in:
1 parent
ca7300ad52
commit
5126ac4592
4 files changed
+113
-5
No files matched your search
@@ -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<HTMLInputElement>(null);
|
||||
const atts = attachments ?? [];
|
||||
// 发送键位由系统设置决定(localStorage),默认 Enter 发送。
|
||||
const sendMode = useChatSendMode();
|
||||
function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<KeyboardIcon className="size-4" />
|
||||
会话输入框发送键位
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
对话页与任务详情的主 Agent 会话输入框共用此设置,选择后立即生效、无需保存。
|
||||
<br />
|
||||
该偏好<b>只存在本浏览器</b>,不随账号同步,换浏览器或清理站点数据后需重新设置。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="chat-send-mode" className="text-sm font-normal text-muted-foreground">
|
||||
发送方式
|
||||
</Label>
|
||||
<Select value={sendMode} onValueChange={(v) => setChatSendMode(v as ChatSendMode)}>
|
||||
<SelectTrigger id="chat-send-mode" className="w-72">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CHAT_SEND_MODE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user