Attach dropped files in every composer (#1750)
* fix(app): attach dropped files in composer Composer parents had to expose attachment callbacks, so New Workspace only wired image drops and JSON files never became file attachments. Centralize dropped-file ingestion in Composer so all composer surfaces share the same image and file path. * fix(app): handle desktop dropped attachment paths
This commit is contained in:
@@ -14,18 +14,7 @@ import { AgentStreamView, type AgentStreamViewHandle } from "@/agent-stream/view
|
||||
import { ArchivedAgentCallout } from "@/components/archived-agent-callout";
|
||||
import { Composer } from "@/composer";
|
||||
import { AgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||
import { FileDropZone } from "@/components/file-drop-zone";
|
||||
import { uploadFileAttachments } from "@/composer/actions";
|
||||
import {
|
||||
getMimeTypeFromPath,
|
||||
isRasterImageFile,
|
||||
isRasterImagePath,
|
||||
} from "@/attachments/file-types";
|
||||
import { readDesktopFileBytes } from "@/hooks/use-file-picker";
|
||||
import type { DroppedItem } from "@/hooks/use-file-drop-zone";
|
||||
import type { UserComposerAttachment } from "@/attachments/types";
|
||||
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
||||
import type { ImageAttachment } from "@/composer/types";
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
import {
|
||||
ToastViewport,
|
||||
@@ -706,8 +695,6 @@ function ChatAgentContent({
|
||||
const { api: toastApi, toast: toastState, dismiss: dismissToast } = useToastHost();
|
||||
const { isArchivingAgent } = useArchiveAgent();
|
||||
const streamViewRef = useRef<AgentStreamViewHandle>(null);
|
||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||
const addFilesRef = useRef<((files: UserComposerAttachment[]) => void) | null>(null);
|
||||
const clearOnAgentBlurRef = useRef<() => void>(() => {});
|
||||
const wasPaneFocusedRef = useRef(isPaneFocused);
|
||||
const reconnectToastArmedRef = useRef(false);
|
||||
@@ -716,54 +703,6 @@ function ChatAgentContent({
|
||||
routeKey: string;
|
||||
reason: "initial-entry" | "resume";
|
||||
} | null>(null);
|
||||
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
||||
addImagesRef.current?.(files);
|
||||
}, []);
|
||||
|
||||
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
||||
addImagesRef.current = addImages;
|
||||
}, []);
|
||||
|
||||
const handleAddFilesCallback = useCallback(
|
||||
(addFiles: (files: UserComposerAttachment[]) => void) => {
|
||||
addFilesRef.current = addFiles;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleGenericFilesDropped = useCallback(
|
||||
async (items: DroppedItem[]) => {
|
||||
if (!client || !isConnected) return;
|
||||
const nonImageItems = items.filter((item) => {
|
||||
if (item.kind === "web-file") return !isRasterImageFile(item.file);
|
||||
return !isRasterImagePath(item.path);
|
||||
});
|
||||
if (nonImageItems.length === 0) return;
|
||||
try {
|
||||
const files = await Promise.all(
|
||||
nonImageItems.map(async (item) => {
|
||||
if (item.kind === "web-file") {
|
||||
return {
|
||||
fileName: item.file.name,
|
||||
mimeType: item.file.type || getMimeTypeFromPath(item.file.name),
|
||||
bytes: new Uint8Array(await item.file.arrayBuffer()),
|
||||
};
|
||||
}
|
||||
const fileName = item.path.split("/").pop() ?? item.path.split("\\").pop() ?? item.path;
|
||||
const bytes = await readDesktopFileBytes(item.path);
|
||||
return { fileName, mimeType: getMimeTypeFromPath(item.path), bytes };
|
||||
}),
|
||||
);
|
||||
const uploaded = await uploadFileAttachments({ client, files });
|
||||
addFilesRef.current?.(uploaded);
|
||||
} catch (error) {
|
||||
console.error("[AgentPanel] Failed to upload dropped files:", error);
|
||||
toastApi.error(error instanceof Error ? error.message : "Failed to upload file");
|
||||
}
|
||||
},
|
||||
[client, isConnected, toastApi],
|
||||
);
|
||||
|
||||
const agentState = useSessionStore(
|
||||
useShallow((state) => selectChatAgentState(state, serverId, agentId)),
|
||||
);
|
||||
@@ -1121,10 +1060,6 @@ function ChatAgentContent({
|
||||
dismiss={dismissToast}
|
||||
streamViewRef={streamViewRef}
|
||||
animatedContentStyle={animatedContentStyle}
|
||||
handleFilesDropped={handleFilesDropped}
|
||||
handleGenericFilesDropped={handleGenericFilesDropped}
|
||||
handleAddImagesCallback={handleAddImagesCallback}
|
||||
handleAddFilesCallback={handleAddFilesCallback}
|
||||
handleComposerHeightChange={handleComposerHeightChange}
|
||||
handleMessageSent={handleMessageSent}
|
||||
showHistorySyncOverlay={showHistorySyncOverlay}
|
||||
@@ -1150,10 +1085,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
dismiss,
|
||||
streamViewRef,
|
||||
animatedContentStyle,
|
||||
handleFilesDropped,
|
||||
handleGenericFilesDropped,
|
||||
handleAddImagesCallback,
|
||||
handleAddFilesCallback,
|
||||
handleComposerHeightChange,
|
||||
handleMessageSent,
|
||||
showHistorySyncOverlay,
|
||||
@@ -1175,10 +1106,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
dismiss: () => void;
|
||||
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
||||
animatedContentStyle: object[];
|
||||
handleFilesDropped: (files: ImageAttachment[]) => void;
|
||||
handleGenericFilesDropped: (items: DroppedItem[]) => void;
|
||||
handleAddImagesCallback: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||
handleAddFilesCallback: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||
handleComposerHeightChange: (height: number) => void;
|
||||
handleMessageSent: () => void;
|
||||
showHistorySyncOverlay: boolean;
|
||||
@@ -1237,8 +1164,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
agentInputDraft={agentInputDraft}
|
||||
onAttentionInputFocus={onAttentionInputFocus}
|
||||
onAttentionPromptSend={onAttentionPromptSend}
|
||||
onAddImages={handleAddImagesCallback}
|
||||
onAddFiles={handleAddFilesCallback}
|
||||
onComposerHeightChange={handleComposerHeightChange}
|
||||
onMessageSent={handleMessageSent}
|
||||
/>
|
||||
@@ -1252,25 +1177,19 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
return (
|
||||
<RewindComposerRestoreProvider text={agentInputDraft.text} setText={agentInputDraft.setText}>
|
||||
<View style={styles.root}>
|
||||
<FileDropZone
|
||||
onFilesDropped={handleFilesDropped}
|
||||
onGenericFilesDropped={handleGenericFilesDropped}
|
||||
disabled={isArchivingCurrentAgent}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{contentContainer}
|
||||
<View style={styles.container}>
|
||||
{contentContainer}
|
||||
|
||||
{composerSection}
|
||||
{composerSection}
|
||||
|
||||
{showHistorySyncOverlay ? (
|
||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||
</View>
|
||||
) : null}
|
||||
{showHistorySyncOverlay ? (
|
||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<ToastViewport toast={toast} onDismiss={dismiss} placement="panel" />
|
||||
</View>
|
||||
</FileDropZone>
|
||||
<ToastViewport toast={toast} onDismiss={dismiss} placement="panel" />
|
||||
</View>
|
||||
|
||||
{isArchivingCurrentAgent ? (
|
||||
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
||||
@@ -1361,8 +1280,6 @@ const AgentComposerSection = memo(function AgentComposerSection({
|
||||
agentInputDraft,
|
||||
onAttentionInputFocus,
|
||||
onAttentionPromptSend,
|
||||
onAddImages,
|
||||
onAddFiles,
|
||||
onComposerHeightChange,
|
||||
onMessageSent,
|
||||
}: {
|
||||
@@ -1376,8 +1293,6 @@ const AgentComposerSection = memo(function AgentComposerSection({
|
||||
agentInputDraft: AgentInputDraft;
|
||||
onAttentionInputFocus: () => void;
|
||||
onAttentionPromptSend: () => void;
|
||||
onAddImages: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||
onAddFiles: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||
onComposerHeightChange: (height: number) => void;
|
||||
onMessageSent: () => void;
|
||||
}) {
|
||||
@@ -1401,8 +1316,6 @@ const AgentComposerSection = memo(function AgentComposerSection({
|
||||
agentInputDraft={agentInputDraft}
|
||||
onAttentionInputFocus={onAttentionInputFocus}
|
||||
onAttentionPromptSend={onAttentionPromptSend}
|
||||
onAddImages={onAddImages}
|
||||
onAddFiles={onAddFiles}
|
||||
onComposerHeightChange={onComposerHeightChange}
|
||||
onMessageSent={onMessageSent}
|
||||
/>
|
||||
@@ -1418,8 +1331,6 @@ function ActiveAgentComposer({
|
||||
agentInputDraft,
|
||||
onAttentionInputFocus,
|
||||
onAttentionPromptSend,
|
||||
onAddImages,
|
||||
onAddFiles,
|
||||
onComposerHeightChange,
|
||||
onMessageSent,
|
||||
}: {
|
||||
@@ -1431,8 +1342,6 @@ function ActiveAgentComposer({
|
||||
agentInputDraft: AgentInputDraft;
|
||||
onAttentionInputFocus: () => void;
|
||||
onAttentionPromptSend: () => void;
|
||||
onAddImages: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||
onAddFiles: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||
onComposerHeightChange: (height: number) => void;
|
||||
onMessageSent: () => void;
|
||||
}) {
|
||||
@@ -1578,8 +1487,6 @@ function ActiveAgentComposer({
|
||||
isSubmitLoading={isSubmitLoading}
|
||||
onAttentionInputFocus={onAttentionInputFocus}
|
||||
onAttentionPromptSend={onAttentionPromptSend}
|
||||
onAddImages={onAddImages}
|
||||
onAddFiles={onAddFiles}
|
||||
onComposerHeightChange={onComposerHeightChange}
|
||||
onMessageSent={onMessageSent}
|
||||
onClientSlashCommand={handleClientSlashCommand}
|
||||
|
||||
Reference in New Issue
Block a user