源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user