源码快照 @ 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:
@@ -0,0 +1,265 @@
|
||||
import { useEffect, useState, type RefObject } from "react";
|
||||
import type {
|
||||
AgentRichTextEditorHandle,
|
||||
AgentRichTextPastedImage
|
||||
} from "../agentRichText/AgentRichTextEditor";
|
||||
import {
|
||||
imageFilesFromDataTransfer,
|
||||
nonImageFilesFromDataTransfer,
|
||||
readAgentRichTextPromptImages,
|
||||
systemFileDragInfoFromDataTransfer
|
||||
} from "../agentRichText/agentRichTextPromptImages";
|
||||
import { hasWorkspaceFileDropData } from "../../terminalNode/workspaceFileDrop";
|
||||
|
||||
interface UseComposerFileDropInput {
|
||||
composerRef: RefObject<HTMLFormElement | null>;
|
||||
editorHandleRef: RefObject<AgentRichTextEditorHandle | null>;
|
||||
inputDisabled: boolean;
|
||||
promptFilesSupported: boolean;
|
||||
promptImagesSupported: boolean;
|
||||
addDraftImages: (images: AgentRichTextPastedImage[]) => void;
|
||||
addDraftFiles: (files: readonly File[]) => void;
|
||||
scheduleComposerFocus: () => void;
|
||||
onPromptImagesUnsupported?: () => void;
|
||||
}
|
||||
|
||||
function isPointInsideElement(
|
||||
element: HTMLElement,
|
||||
clientX: number,
|
||||
clientY: number
|
||||
): boolean {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return (
|
||||
clientX >= bounds.left &&
|
||||
clientX <= bounds.right &&
|
||||
clientY >= bounds.top &&
|
||||
clientY <= bounds.bottom
|
||||
);
|
||||
}
|
||||
|
||||
export function useComposerFileDrop({
|
||||
composerRef,
|
||||
editorHandleRef,
|
||||
inputDisabled,
|
||||
promptFilesSupported,
|
||||
promptImagesSupported,
|
||||
addDraftImages,
|
||||
addDraftFiles,
|
||||
scheduleComposerFocus,
|
||||
onPromptImagesUnsupported
|
||||
}: UseComposerFileDropInput) {
|
||||
const [fileDropOverlayHost, setFileDropOverlayHost] =
|
||||
useState<HTMLElement | null>(null);
|
||||
const [fileDropOverlayActive, setFileDropOverlayActive] = useState(false);
|
||||
useEffect(() => {
|
||||
const composer = composerRef.current;
|
||||
const dropTarget = composer?.closest("#agent-gui-detail") ?? composer;
|
||||
if (!dropTarget) {
|
||||
return undefined;
|
||||
}
|
||||
let isDisposed = false;
|
||||
setFileDropOverlayHost(dropTarget as HTMLElement);
|
||||
|
||||
const isDragEvent = (event: Event): event is DragEvent =>
|
||||
"dataTransfer" in event;
|
||||
|
||||
const clearDropOverlay = (): void => {
|
||||
if (!isDisposed) {
|
||||
setFileDropOverlayActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const containsEventTarget = (event: DragEvent): boolean => {
|
||||
const target = event.target;
|
||||
return target instanceof Node && dropTarget.contains(target);
|
||||
};
|
||||
|
||||
const systemFileDrag = (
|
||||
event: DragEvent
|
||||
): { hasImageFiles: boolean; hasRegularFiles: boolean } | null => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
inputDisabled ||
|
||||
!containsEventTarget(event) ||
|
||||
hasWorkspaceFileDropData(event.dataTransfer)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const dragInfo = systemFileDragInfoFromDataTransfer(event.dataTransfer);
|
||||
const hasRegularFiles = dragInfo.hasRegularFiles && promptFilesSupported;
|
||||
const hasPromptImages = dragInfo.hasImageFiles && promptImagesSupported;
|
||||
const hasImageFilesAsFiles =
|
||||
dragInfo.hasImageFiles &&
|
||||
!promptImagesSupported &&
|
||||
promptFilesSupported;
|
||||
if (!hasPromptImages && !hasImageFilesAsFiles && !hasRegularFiles) {
|
||||
return null;
|
||||
}
|
||||
return { hasImageFiles: dragInfo.hasImageFiles, hasRegularFiles };
|
||||
};
|
||||
|
||||
const systemFileDrop = (
|
||||
event: DragEvent
|
||||
): { imageFiles: File[]; regularFiles: File[] } | null => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
inputDisabled ||
|
||||
!containsEventTarget(event) ||
|
||||
hasWorkspaceFileDropData(event.dataTransfer)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const imageFiles = imageFilesFromDataTransfer(event.dataTransfer);
|
||||
const imageFileSet = new Set(imageFiles);
|
||||
const regularFiles = promptFilesSupported
|
||||
? [
|
||||
...nonImageFilesFromDataTransfer(event.dataTransfer).filter(
|
||||
(file) => !imageFileSet.has(file)
|
||||
),
|
||||
...(!promptImagesSupported ? imageFiles : [])
|
||||
]
|
||||
: [];
|
||||
const promptImageFiles = promptImagesSupported ? imageFiles : [];
|
||||
const unsupportedImageFiles =
|
||||
!promptImagesSupported && !promptFilesSupported ? imageFiles : [];
|
||||
if (
|
||||
promptImageFiles.length === 0 &&
|
||||
regularFiles.length === 0 &&
|
||||
unsupportedImageFiles.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
imageFiles: [...promptImageFiles, ...unsupportedImageFiles],
|
||||
regularFiles
|
||||
};
|
||||
};
|
||||
|
||||
const handleDragOver: EventListener = (event): void => {
|
||||
if (!isDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
const drag = systemFileDrag(event);
|
||||
if (!drag) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (
|
||||
!drag.hasRegularFiles &&
|
||||
drag.hasImageFiles &&
|
||||
!promptImagesSupported &&
|
||||
!promptFilesSupported
|
||||
) {
|
||||
clearDropOverlay();
|
||||
return;
|
||||
}
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
if (!isDisposed) {
|
||||
setFileDropOverlayActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop: EventListener = (event): void => {
|
||||
if (!isDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
const drop = systemFileDrop(event);
|
||||
if (!drop) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clearDropOverlay();
|
||||
if (drop.regularFiles.length > 0) {
|
||||
editorHandleRef.current?.focusAtEnd();
|
||||
addDraftFiles(drop.regularFiles);
|
||||
scheduleComposerFocus();
|
||||
}
|
||||
if (drop.imageFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!promptImagesSupported) {
|
||||
onPromptImagesUnsupported?.();
|
||||
return;
|
||||
}
|
||||
void readAgentRichTextPromptImages(drop.imageFiles).then((images) => {
|
||||
if (isDisposed || images.length === 0) {
|
||||
return;
|
||||
}
|
||||
addDraftImages(images);
|
||||
scheduleComposerFocus();
|
||||
});
|
||||
};
|
||||
|
||||
// `dragleave` is unreliable across nested children, so mirror the file
|
||||
// manager and clear the overlay from a capture-phase document listener
|
||||
// whenever the pointer leaves the drop target's bounds.
|
||||
const handleDocumentDragOver: EventListener = (event): void => {
|
||||
if (!isDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isPointInsideElement(
|
||||
dropTarget as HTMLElement,
|
||||
event.clientX,
|
||||
event.clientY
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
clearDropOverlay();
|
||||
};
|
||||
|
||||
// Once the drag leaves the window entirely, `dragover` stops firing (so the
|
||||
// handler above can never fire) and external file drags never dispatch a
|
||||
// renderer-side `dragend` — leaving the overlay stuck. A `dragleave` whose
|
||||
// pointer sits at/outside the viewport edge (or has no relatedTarget) means
|
||||
// the cursor left the window, so clear the overlay then.
|
||||
const handleDocumentDragLeave: EventListener = (event): void => {
|
||||
if (!isDragEvent(event)) {
|
||||
return;
|
||||
}
|
||||
const leftWindow =
|
||||
event.relatedTarget === null ||
|
||||
event.clientX <= 0 ||
|
||||
event.clientY <= 0 ||
|
||||
event.clientX >= window.innerWidth ||
|
||||
event.clientY >= window.innerHeight;
|
||||
if (leftWindow) {
|
||||
clearDropOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
dropTarget.addEventListener("dragover", handleDragOver);
|
||||
dropTarget.addEventListener("drop", handleDrop);
|
||||
document.addEventListener("dragover", handleDocumentDragOver, true);
|
||||
document.addEventListener("dragleave", handleDocumentDragLeave, true);
|
||||
window.addEventListener("dragend", clearDropOverlay);
|
||||
window.addEventListener("drop", clearDropOverlay);
|
||||
window.addEventListener("blur", clearDropOverlay);
|
||||
return () => {
|
||||
isDisposed = true;
|
||||
setFileDropOverlayHost(null);
|
||||
setFileDropOverlayActive(false);
|
||||
dropTarget.removeEventListener("dragover", handleDragOver);
|
||||
dropTarget.removeEventListener("drop", handleDrop);
|
||||
document.removeEventListener("dragover", handleDocumentDragOver, true);
|
||||
document.removeEventListener("dragleave", handleDocumentDragLeave, true);
|
||||
window.removeEventListener("dragend", clearDropOverlay);
|
||||
window.removeEventListener("drop", clearDropOverlay);
|
||||
window.removeEventListener("blur", clearDropOverlay);
|
||||
};
|
||||
}, [
|
||||
addDraftImages,
|
||||
addDraftFiles,
|
||||
inputDisabled,
|
||||
onPromptImagesUnsupported,
|
||||
promptFilesSupported,
|
||||
promptImagesSupported,
|
||||
scheduleComposerFocus
|
||||
]);
|
||||
|
||||
return { fileDropOverlayActive, fileDropOverlayHost };
|
||||
}
|
||||
Reference in New Issue
Block a user