Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIRenameConversationDialog.tsx
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

196 lines
6.5 KiB
TypeScript

import { memo, useCallback, useEffect, useRef, useState } from "react";
import { ConfirmationDialog, Input } from "@tutti-os/ui-system";
import { Button } from "../../../app/renderer/components/ui/button";
import type { AgentGUINodeViewModel } from "../model/agentGuiNodeTypes";
import type { AgentGUIViewLabels } from "./AgentGUINodeView.types";
interface AgentGUIRenameConversationDialogProps {
conversation: AgentGUINodeViewModel["rail"]["conversations"][number] | null;
open: boolean;
labels: AgentGUIViewLabels;
onOpenChange: (open: boolean) => void;
onRename: (agentSessionId: string, title: string) => Promise<void>;
}
export const AgentGUIRenameConversationDialog = memo(
function AgentGUIRenameConversationDialog({
conversation,
open,
labels,
onOpenChange,
onRename
}: AgentGUIRenameConversationDialogProps): React.JSX.Element {
"use memo";
const [title, setTitle] = useState("");
const [isSaving, setIsSaving] = useState(false);
const isSavingRef = useRef(false);
const armedPointerActionRef = useRef<"cancel" | "confirm" | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const trimmedTitle = title.trim();
useEffect(() => {
if (!open || !conversation) {
isSavingRef.current = false;
armedPointerActionRef.current = null;
setIsSaving(false);
return;
}
setTitle(conversation.title);
}, [conversation, open]);
useEffect(() => {
if (!open) {
return;
}
// timing: defer focus until after the dialog's open animation mounts the input
const timer = window.setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 0);
return () => window.clearTimeout(timer);
}, [open, conversation?.id]);
const closeRenameDialog = useCallback(() => {
if (!isSavingRef.current) {
onOpenChange(false);
}
}, [onOpenChange]);
const confirmRename = useCallback(() => {
if (!conversation || isSavingRef.current || !trimmedTitle) {
return;
}
isSavingRef.current = true;
setIsSaving(true);
void onRename(conversation.id, trimmedTitle)
.then(() => {
onOpenChange(false);
})
.catch(() => {
inputRef.current?.focus();
})
.finally(() => {
isSavingRef.current = false;
setIsSaving(false);
});
}, [conversation, onOpenChange, onRename, trimmedTitle]);
return (
<ConfirmationDialog
cancelLabel={labels.cancel}
className="sm:max-w-[480px]"
confirmBusy={isSaving}
confirmDisabled={!trimmedTitle}
confirmLabel={labels.renameSessionSave}
description={labels.renameSessionDescription}
footer={
<div className="flex justify-end gap-2">
<Button
disabled={isSaving}
size="dialog"
type="button"
variant="ghost"
onClick={closeRenameDialog}
onLostPointerCapture={() => {
if (armedPointerActionRef.current === "cancel") {
armedPointerActionRef.current = null;
}
}}
onPointerCancel={() => {
if (armedPointerActionRef.current === "cancel") {
armedPointerActionRef.current = null;
}
}}
onPointerDown={(event) => {
if (event.button === 0) {
armedPointerActionRef.current = "cancel";
}
}}
onPointerLeave={() => {
if (armedPointerActionRef.current === "cancel") {
armedPointerActionRef.current = null;
}
}}
onPointerUp={(event) => {
const isArmed = armedPointerActionRef.current === "cancel";
armedPointerActionRef.current = null;
if (event.button === 0 && isArmed) {
closeRenameDialog();
}
}}
>
{labels.cancel}
</Button>
<Button
className="shadow-none"
disabled={isSaving || !trimmedTitle}
size="dialog"
type="button"
variant="default"
onClick={(event) => {
if (event.detail !== 0) {
return;
}
confirmRename();
}}
onKeyDown={(event) => {
if (
(event.key === "Enter" || event.key === " ") &&
!event.repeat
) {
event.preventDefault();
confirmRename();
}
}}
onLostPointerCapture={() => {
if (armedPointerActionRef.current === "confirm") {
armedPointerActionRef.current = null;
}
}}
onPointerCancel={() => {
if (armedPointerActionRef.current === "confirm") {
armedPointerActionRef.current = null;
}
}}
onPointerDown={(event) => {
if (event.button === 0) {
armedPointerActionRef.current = "confirm";
}
}}
onPointerLeave={() => {
if (armedPointerActionRef.current === "confirm") {
armedPointerActionRef.current = null;
}
}}
onPointerUp={(event) => {
const isArmed = armedPointerActionRef.current === "confirm";
armedPointerActionRef.current = null;
if (event.button === 0 && isArmed) {
confirmRename();
}
}}
>
{labels.renameSessionSave}
</Button>
</div>
}
open={open}
title={labels.renameSessionTitle}
onConfirm={confirmRename}
onOpenChange={onOpenChange}
>
<Input
ref={inputRef}
aria-label={labels.renameSessionTitle}
className="h-9"
variant="md"
placeholder={labels.renameSessionPlaceholder}
value={title}
onChange={(event) => setTitle(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
confirmRename();
}
}}
/>
</ConfirmationDialog>
);
}
);