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
196 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
);
|