mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* fix(views): guard IME composition on Enter-to-submit handlers Chinese/Japanese/Korean IMEs use Enter to commit a multi-key composition. When that Enter also triggers a submit/create handler, the form fires before the user has finished typing. Add a shared `isImeComposing` predicate in @multica/core/utils that checks both `nativeEvent.isComposing` and `keyCode === 229` (Safari clears isComposing on the commit keydown but keyCode stays 229). Apply the guard to every Enter→action handler in packages/views where the input can hold IME text: workspace name, agent name/description, skill name, label name/edit, mention suggestion picker, property picker search, delete-workspace typed confirmation. Tiptap submit-shortcut already guards via `view.composing`; left as is. Skipped numeric/email/URL/file-path inputs where IME does not apply. Co-authored-by: multica-agent <github@multica.ai> * style(agents): align Escape handling with early return in inspector Three onKeyDown handlers in agent-detail-inspector.tsx now follow the same shape as labels-panel: handle Escape with an explicit return, then the IME guard, then Enter submit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from "@multica/ui/components/ui/dialog";
|
|
import { Input } from "@multica/ui/components/ui/input";
|
|
import { Label } from "@multica/ui/components/ui/label";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { isImeComposing } from "@multica/core/utils";
|
|
import { useT } from "../../i18n";
|
|
|
|
/**
|
|
* Typed-confirmation dialog for workspace deletion — GitHub's repo-delete
|
|
* pattern. The destructive button stays disabled until the user types
|
|
* the workspace name exactly (case-sensitive, no trimming). The friction
|
|
* is deliberate: deleting a workspace cascades into every issue, agent,
|
|
* skill, and run under it, and the backend has no soft-delete.
|
|
*
|
|
* Case-sensitive match matches GitHub's pattern and catches the "I
|
|
* remember the gist of the name but not the casing" misfire. No trim —
|
|
* leading/trailing whitespace indicates a typo, and silently accepting
|
|
* it would weaken the whole point of the gate.
|
|
*
|
|
* Input value resets whenever the dialog closes so reopening doesn't
|
|
* leak the previous attempt (which might have been for a different
|
|
* workspace after a swap).
|
|
*/
|
|
export function DeleteWorkspaceDialog({
|
|
workspaceName,
|
|
loading = false,
|
|
open,
|
|
onOpenChange,
|
|
onConfirm,
|
|
}: {
|
|
workspaceName: string;
|
|
loading?: boolean;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onConfirm: () => void;
|
|
}) {
|
|
const { t } = useT("settings");
|
|
const [typed, setTyped] = useState("");
|
|
const matched = typed === workspaceName;
|
|
|
|
// Reset on close (so reopening for a different workspace doesn't leak
|
|
// the prior attempt) AND on workspaceName change (if another owner
|
|
// renames the workspace while the dialog is open, the already-typed
|
|
// string stops matching and there'd be no feedback explaining why).
|
|
useEffect(() => {
|
|
setTyped("");
|
|
}, [open, workspaceName]);
|
|
|
|
const submit = () => {
|
|
if (!matched || loading) return;
|
|
onConfirm();
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t(($) => $.delete_workspace_dialog.title)}</DialogTitle>
|
|
<DialogDescription>
|
|
{t(($) => $.delete_workspace_dialog.description)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="delete-workspace-confirm" className="text-xs">
|
|
{t(($) => $.delete_workspace_dialog.type_to_confirm_prefix)}{" "}
|
|
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
|
|
{workspaceName}
|
|
</code>{" "}
|
|
{t(($) => $.delete_workspace_dialog.type_to_confirm_suffix)}
|
|
</Label>
|
|
<Input
|
|
id="delete-workspace-confirm"
|
|
value={typed}
|
|
onChange={(e) => setTyped(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (isImeComposing(e)) return;
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
submit();
|
|
}
|
|
}}
|
|
placeholder={workspaceName}
|
|
autoFocus
|
|
disabled={loading}
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck={false}
|
|
/>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={loading}
|
|
>
|
|
{t(($) => $.delete_workspace_dialog.cancel)}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
onClick={submit}
|
|
disabled={!matched || loading}
|
|
>
|
|
{loading ? t(($) => $.delete_workspace_dialog.deleting) : t(($) => $.delete_workspace_dialog.confirm)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|