Files
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

189 lines
5.9 KiB
TypeScript

import { type PointerEvent, useRef, useState } from "react";
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Spinner,
Textarea
} from "@tutti-os/ui-system";
import type { AgentHostQuickPrompt } from "../../../../host/agentHostApi";
import type {
AgentQuickPromptDraft,
AgentQuickPromptLibraryController
} from "./useAgentQuickPromptLibrary";
const MAX_TITLE_CODE_POINTS = 80;
const MAX_CONTENT_BYTES = 32 * 1024;
export function AgentQuickPromptEditorDialog({
controller
}: {
controller: AgentQuickPromptLibraryController;
}): React.JSX.Element {
const { labels } = controller;
const source = controller.mode === "edit" ? controller.selectedPrompt : null;
const [draft, setDraft] = useState<AgentQuickPromptDraft>(
() => controller.initialDraft ?? promptDraft(source)
);
const [validationError, setValidationError] = useState<string | null>(null);
const cancelRequestedOnPointerDownRef = useRef(false);
const submit = async (): Promise<void> => {
const error = validateDraft(draft, labels);
if (error) {
setValidationError(error);
return;
}
setValidationError(null);
await controller.saveDraft({
title: draft.title.trim(),
content: draft.content
});
};
const mutationError =
controller.mutationError === "conflict"
? labels.conflict
: controller.mutationError === "generic"
? labels.mutationError
: null;
return (
<Dialog
open={controller.isEditorOpen}
onOpenChange={(open) => {
if (!open && !controller.isSaving) {
controller.closeDialog();
}
}}
>
<DialogContent
className="sm:max-w-[560px]"
showCloseButton={false}
onKeyDownCapture={(event) => {
// The dialog is portalled but still participates in the Composer's
// React event tree. Keep Enter inside the form so a keyboard save
// cannot reach the Composer submit shortcut after the dialog closes.
if (event.key === "Enter") event.stopPropagation();
}}
onEscapeKeyDown={(event) => {
if (controller.isSaving) event.preventDefault();
}}
onInteractOutside={(event) => {
if (controller.isSaving) event.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>
{source ? labels.editTitle : labels.createTitle}
</DialogTitle>
<DialogDescription className="sr-only">
{labels.contentPlaceholder}
</DialogDescription>
</DialogHeader>
<form
className="flex min-h-0 flex-col gap-3"
onSubmit={(event) => {
event.preventDefault();
void submit();
}}
>
<label className="flex flex-col gap-1.5 text-[13px] text-[var(--text-secondary)]">
<span>{labels.titleLabel}</span>
<Input
autoFocus
aria-invalid={Boolean(validationError)}
disabled={controller.isSaving}
maxLength={MAX_TITLE_CODE_POINTS * 2}
placeholder={labels.titlePlaceholder}
value={draft.title}
onChange={(event) =>
setDraft((current) => ({
...current,
title: event.target.value
}))
}
/>
</label>
<label className="flex min-h-0 flex-col gap-1.5 text-[13px] text-[var(--text-secondary)]">
<span>{labels.contentLabel}</span>
<Textarea
aria-invalid={Boolean(validationError)}
className="min-h-[128px] resize-y"
disabled={controller.isSaving}
placeholder={labels.contentPlaceholder}
value={draft.content}
onChange={(event) =>
setDraft((current) => ({
...current,
content: event.target.value
}))
}
/>
</label>
{validationError || mutationError ? (
<p className="text-[12px] text-[var(--state-danger)]" role="alert">
{validationError ?? mutationError}
</p>
) : null}
<DialogFooter>
<Button
disabled={controller.isSaving}
size="dialog"
type="button"
variant="ghost"
onPointerDown={(event: PointerEvent<HTMLButtonElement>) => {
if (event.button !== 0) return;
cancelRequestedOnPointerDownRef.current = true;
event.preventDefault();
controller.closeDialog();
}}
onClick={() => {
if (cancelRequestedOnPointerDownRef.current) {
cancelRequestedOnPointerDownRef.current = false;
return;
}
controller.closeDialog();
}}
>
{labels.cancel}
</Button>
<Button disabled={controller.isSaving} size="dialog" type="submit">
{controller.isSaving ? <Spinner size={14} /> : null}
{controller.isSaving ? labels.saving : labels.save}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
function promptDraft(
source: AgentHostQuickPrompt | null
): AgentQuickPromptDraft {
return source
? { title: source.title, content: source.content }
: { title: "", content: "" };
}
function validateDraft(
draft: AgentQuickPromptDraft,
labels: AgentQuickPromptLibraryController["labels"]
): string | null {
if (!draft.title.trim() || !draft.content.trim()) {
return labels.required;
}
if (Array.from(draft.title.trim()).length > MAX_TITLE_CODE_POINTS) {
return labels.titleTooLong;
}
if (new TextEncoder().encode(draft.content).byteLength > MAX_CONTENT_BYTES) {
return labels.contentTooLarge;
}
return null;
}