feat(app): add drag-and-drop file upload and file picker to composer (#1474)
- Desktop web: drag-and-drop non-image files onto the chat pane to upload - Desktop web: file picker option in the composer attachment sheet - Uploaded files are sent to the LLM as filename references in a tmp directory - New useFilePicker hook for cross-platform file picking - Extended useFileDropZone to support generic file drops - Added UploadedFileAttachment type to protocol and composer attachment system - Fixed adaptive modal sheet closing on backdrop press in selection mode
This commit is contained in:
@@ -1,4 +1,8 @@
|
|||||||
import type { AgentAttachment, GitHubSearchItem } from "@getpaseo/protocol/messages";
|
import type {
|
||||||
|
AgentAttachment,
|
||||||
|
GitHubSearchItem,
|
||||||
|
UploadedFileAttachment,
|
||||||
|
} from "@getpaseo/protocol/messages";
|
||||||
|
|
||||||
export type AttachmentStorageType = "web-indexeddb" | "desktop-file" | "native-file";
|
export type AttachmentStorageType = "web-indexeddb" | "desktop-file" | "native-file";
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ export type PullRequestContextAttachment =
|
|||||||
|
|
||||||
export type UserComposerAttachment =
|
export type UserComposerAttachment =
|
||||||
| { kind: "image"; metadata: AttachmentMetadata }
|
| { kind: "image"; metadata: AttachmentMetadata }
|
||||||
|
| { kind: "file"; attachment: UploadedFileAttachment }
|
||||||
| { kind: "github_issue"; item: GitHubSearchItem }
|
| { kind: "github_issue"; item: GitHubSearchItem }
|
||||||
| { kind: "github_pr"; item: GitHubSearchItem };
|
| { kind: "github_pr"; item: GitHubSearchItem };
|
||||||
|
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ export interface AdaptiveModalSheetProps {
|
|||||||
desktopMaxWidth?: number;
|
desktopMaxWidth?: number;
|
||||||
/** When provided, wraps the card content in a FileDropZone. */
|
/** When provided, wraps the card content in a FileDropZone. */
|
||||||
onFilesDropped?: (files: ImageAttachment[]) => void;
|
onFilesDropped?: (files: ImageAttachment[]) => void;
|
||||||
|
onGenericFilesDropped?: (items: import("@/hooks/use-file-drop-zone").DroppedItem[]) => void;
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
presentation?: "push" | "replace";
|
presentation?: "push" | "replace";
|
||||||
}
|
}
|
||||||
@@ -467,6 +468,7 @@ export function AdaptiveModalSheet({
|
|||||||
testID,
|
testID,
|
||||||
desktopMaxWidth,
|
desktopMaxWidth,
|
||||||
onFilesDropped,
|
onFilesDropped,
|
||||||
|
onGenericFilesDropped,
|
||||||
scrollable = true,
|
scrollable = true,
|
||||||
presentation,
|
presentation,
|
||||||
}: AdaptiveModalSheetProps) {
|
}: AdaptiveModalSheetProps) {
|
||||||
@@ -602,7 +604,12 @@ export function AdaptiveModalSheet({
|
|||||||
/>
|
/>
|
||||||
<View style={desktopCardStyle}>
|
<View style={desktopCardStyle}>
|
||||||
{onFilesDropped ? (
|
{onFilesDropped ? (
|
||||||
<FileDropZone onFilesDropped={onFilesDropped}>{cardInner}</FileDropZone>
|
<FileDropZone
|
||||||
|
onFilesDropped={onFilesDropped}
|
||||||
|
onGenericFilesDropped={onGenericFilesDropped}
|
||||||
|
>
|
||||||
|
{cardInner}
|
||||||
|
</FileDropZone>
|
||||||
) : (
|
) : (
|
||||||
cardInner
|
cardInner
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,23 +4,30 @@ import Animated, { useAnimatedStyle, withTiming, useSharedValue } from "react-na
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Upload } from "lucide-react-native";
|
import { Upload } from "lucide-react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFileDropZone } from "@/hooks/use-file-drop-zone";
|
import { useFileDropZone, type DroppedItem } from "@/hooks/use-file-drop-zone";
|
||||||
import type { ImageAttachment } from "@/composer/types";
|
import type { ImageAttachment } from "@/composer/types";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
interface FileDropZoneProps {
|
interface FileDropZoneProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onFilesDropped: (files: ImageAttachment[]) => void;
|
onFilesDropped: (files: ImageAttachment[]) => void;
|
||||||
|
onGenericFilesDropped?: (items: DroppedItem[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IS_WEB = isWeb;
|
const IS_WEB = isWeb;
|
||||||
|
|
||||||
export function FileDropZone({ children, onFilesDropped, disabled = false }: FileDropZoneProps) {
|
export function FileDropZone({
|
||||||
|
children,
|
||||||
|
onFilesDropped,
|
||||||
|
onGenericFilesDropped,
|
||||||
|
disabled = false,
|
||||||
|
}: FileDropZoneProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { isDragging, containerRef } = useFileDropZone({
|
const { isDragging, containerRef } = useFileDropZone({
|
||||||
onFilesDropped,
|
onFilesDropped,
|
||||||
|
onGenericFilesDropped,
|
||||||
disabled,
|
disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +67,7 @@ export function FileDropZone({ children, onFilesDropped, disabled = false }: Fil
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<View style={styles.overlayContent}>
|
<View style={styles.overlayContent}>
|
||||||
<Upload size={32} color={theme.colors.primary} />
|
<Upload size={32} color={theme.colors.primary} />
|
||||||
<Text style={styles.overlayText}>{t("composer.attachments.dropImagesHere")}</Text>
|
<Text style={styles.overlayText}>{t("composer.attachments.dropFilesHere")}</Text>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ function getUserMessageAttachmentLabel(
|
|||||||
return `Issue #${attachment.number}`;
|
return `Issue #${attachment.number}`;
|
||||||
case "text":
|
case "text":
|
||||||
return attachment.title ?? t("message.attachments.textAttachment");
|
return attachment.title ?? t("message.attachments.textAttachment");
|
||||||
|
case "uploaded_file":
|
||||||
|
return attachment.fileName;
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ function createFakeSendClient(
|
|||||||
throw options.rejection;
|
throw options.rejection;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
uploadFile: async () => ({ requestId: "test", file: null, error: null }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ export interface ComposerSendClient {
|
|||||||
attachments: ReturnType<typeof splitComposerAttachmentsForSubmit>["attachments"];
|
attachments: ReturnType<typeof splitComposerAttachmentsForSubmit>["attachments"];
|
||||||
},
|
},
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
uploadFile: (input: { fileName: string; mimeType: string; bytes: Uint8Array }) => Promise<{
|
||||||
|
requestId: string;
|
||||||
|
file: {
|
||||||
|
type: "uploaded_file";
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
path: string;
|
||||||
|
} | null;
|
||||||
|
error: string | null;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComposerCancelClient {
|
export interface ComposerCancelClient {
|
||||||
@@ -95,6 +107,23 @@ export async function pickAndPersistImages(input: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadFileAttachments(input: {
|
||||||
|
client: ComposerSendClient;
|
||||||
|
files: Array<{ fileName: string; mimeType: string; bytes: Uint8Array }>;
|
||||||
|
}): Promise<Extract<ComposerAttachment, { kind: "file" }>[]> {
|
||||||
|
const result: Extract<ComposerAttachment, { kind: "file" }>[] = [];
|
||||||
|
|
||||||
|
for (const file of input.files) {
|
||||||
|
const response = await input.client.uploadFile(file);
|
||||||
|
if (response.error || !response.file) {
|
||||||
|
throw new Error(response.error ?? "Upload failed.");
|
||||||
|
}
|
||||||
|
result.push({ kind: "file", attachment: response.file });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function removeComposerAttachmentAtIndex<T extends ComposerAttachment>(input: {
|
export function removeComposerAttachmentAtIndex<T extends ComposerAttachment>(input: {
|
||||||
attachments: T[];
|
attachments: T[];
|
||||||
index: number;
|
index: number;
|
||||||
@@ -297,6 +326,9 @@ export function openComposerAttachment(input: OpenComposerAttachmentInput): void
|
|||||||
input.setLightboxMetadata(input.attachment.metadata);
|
input.setLightboxMetadata(input.attachment.metadata);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (input.attachment.kind === "file") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isWorkspaceAttachment(input.attachment)) {
|
if (isWorkspaceAttachment(input.attachment)) {
|
||||||
input.openWorkspaceAttachment({ attachment: input.attachment });
|
input.openWorkspaceAttachment({ attachment: input.attachment });
|
||||||
return;
|
return;
|
||||||
@@ -308,12 +340,18 @@ export function buildGithubAttachment(item: GitHubSearchItem): UserComposerAttac
|
|||||||
return item.kind === "pr" ? { kind: "github_pr", item } : { kind: "github_issue", item };
|
return item.kind === "pr" ? { kind: "github_pr", item } : { kind: "github_issue", item };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGithubAttachment(
|
||||||
|
attachment: UserComposerAttachment,
|
||||||
|
): attachment is Extract<UserComposerAttachment, { kind: "github_issue" } | { kind: "github_pr" }> {
|
||||||
|
return attachment.kind === "github_issue" || attachment.kind === "github_pr";
|
||||||
|
}
|
||||||
|
|
||||||
export function toggleGithubAttachment(
|
export function toggleGithubAttachment(
|
||||||
current: UserComposerAttachment[],
|
current: UserComposerAttachment[],
|
||||||
item: GitHubSearchItem,
|
item: GitHubSearchItem,
|
||||||
): UserComposerAttachment[] {
|
): UserComposerAttachment[] {
|
||||||
const matches = (attachment: UserComposerAttachment) =>
|
const matches = (attachment: UserComposerAttachment) =>
|
||||||
attachment.kind !== "image" &&
|
isGithubAttachment(attachment) &&
|
||||||
attachment.item.kind === item.kind &&
|
attachment.item.kind === item.kind &&
|
||||||
attachment.item.number === item.number;
|
attachment.item.number === item.number;
|
||||||
if (current.some(matches)) {
|
if (current.some(matches)) {
|
||||||
@@ -335,7 +373,7 @@ export function toggleGithubAttachmentFromPicker({
|
|||||||
}: ToggleGithubAttachmentFromPickerInput): UserComposerAttachment[] {
|
}: ToggleGithubAttachmentFromPickerInput): UserComposerAttachment[] {
|
||||||
const existingAttachment = current.find(
|
const existingAttachment = current.find(
|
||||||
(attachment) =>
|
(attachment) =>
|
||||||
attachment.kind !== "image" &&
|
isGithubAttachment(attachment) &&
|
||||||
attachment.item.kind === item.kind &&
|
attachment.item.kind === item.kind &&
|
||||||
attachment.item.number === item.number,
|
attachment.item.number === item.number,
|
||||||
);
|
);
|
||||||
@@ -358,7 +396,7 @@ export function isAttachmentSelectedForGithubItem(
|
|||||||
): boolean {
|
): boolean {
|
||||||
return userAttachmentsOnly(current).some(
|
return userAttachmentsOnly(current).some(
|
||||||
(attachment) =>
|
(attachment) =>
|
||||||
attachment.kind !== "image" &&
|
isGithubAttachment(attachment) &&
|
||||||
attachment.item.kind === item.kind &&
|
attachment.item.kind === item.kind &&
|
||||||
attachment.item.number === item.number,
|
attachment.item.number === item.number,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function splitComposerAttachmentsForSubmit(attachments: ComposerAttachmen
|
|||||||
attachments: AgentAttachment[];
|
attachments: AgentAttachment[];
|
||||||
} {
|
} {
|
||||||
const images: ImageAttachment[] = [];
|
const images: ImageAttachment[] = [];
|
||||||
const reviewAttachments: AgentAttachment[] = [];
|
const agentAttachments: AgentAttachment[] = [];
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
if (attachment.kind === "image") {
|
if (attachment.kind === "image") {
|
||||||
@@ -20,22 +20,27 @@ export function splitComposerAttachmentsForSubmit(attachments: ComposerAttachmen
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attachment.kind === "file") {
|
||||||
|
agentAttachments.push(attachment.attachment);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (isWorkspaceAttachment(attachment)) {
|
if (isWorkspaceAttachment(attachment)) {
|
||||||
const workspaceAttachment = workspaceAttachmentToSubmitAttachment(attachment);
|
const workspaceAttachment = workspaceAttachmentToSubmitAttachment(attachment);
|
||||||
if (workspaceAttachment) {
|
if (workspaceAttachment) {
|
||||||
reviewAttachments.push(workspaceAttachment);
|
agentAttachments.push(workspaceAttachment);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewAttachment = buildGitHubAttachmentFromSearchItem(attachment.item);
|
const reviewAttachment = buildGitHubAttachmentFromSearchItem(attachment.item);
|
||||||
if (reviewAttachment) {
|
if (reviewAttachment) {
|
||||||
reviewAttachments.push(reviewAttachment);
|
agentAttachments.push(reviewAttachment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
images,
|
images,
|
||||||
attachments: reviewAttachments,
|
attachments: agentAttachments,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
AudioLines,
|
AudioLines,
|
||||||
CircleDot,
|
CircleDot,
|
||||||
|
FileText,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Github,
|
Github,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
import { ContextWindowMeter } from "@/components/context-window-meter";
|
import { ContextWindowMeter } from "@/components/context-window-meter";
|
||||||
import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker";
|
import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import { useFilePicker } from "@/hooks/use-file-picker";
|
||||||
import { MessageInput, type MessageInputRef, type AttachmentMenuItem } from "./input/input";
|
import { MessageInput, type MessageInputRef, type AttachmentMenuItem } from "./input/input";
|
||||||
import type { ImageAttachment, MessagePayload } from "./types";
|
import type { ImageAttachment, MessagePayload } from "./types";
|
||||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||||
@@ -59,6 +61,7 @@ import {
|
|||||||
removeComposerAttachmentAtIndex,
|
removeComposerAttachmentAtIndex,
|
||||||
sendQueuedComposerMessageNow,
|
sendQueuedComposerMessageNow,
|
||||||
toggleGithubAttachmentFromPicker,
|
toggleGithubAttachmentFromPicker,
|
||||||
|
uploadFileAttachments,
|
||||||
type AgentStreamWriter,
|
type AgentStreamWriter,
|
||||||
type QueueWriter,
|
type QueueWriter,
|
||||||
type QueuedComposerMessage,
|
type QueuedComposerMessage,
|
||||||
@@ -114,6 +117,7 @@ type AttachmentListUpdater =
|
|||||||
| ((prev: UserComposerAttachment[]) => UserComposerAttachment[]);
|
| ((prev: UserComposerAttachment[]) => UserComposerAttachment[]);
|
||||||
|
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
const noopCallback = () => {};
|
||||||
|
|
||||||
function resolveComposerButtonIconSize(): number {
|
function resolveComposerButtonIconSize(): number {
|
||||||
return isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
|
return isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
|
||||||
@@ -259,6 +263,7 @@ interface RenderAttachmentTrayArgs {
|
|||||||
labels: {
|
labels: {
|
||||||
openImage: string;
|
openImage: string;
|
||||||
removeImage: string;
|
removeImage: string;
|
||||||
|
removeFile: string;
|
||||||
openGithub: (kind: string, number: number) => string;
|
openGithub: (kind: string, number: number) => string;
|
||||||
removeGithub: (kind: string, number: number) => string;
|
removeGithub: (kind: string, number: number) => string;
|
||||||
};
|
};
|
||||||
@@ -359,6 +364,18 @@ function renderComposerAttachmentPill(args: RenderComposerAttachmentPillArgs): R
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (attachment.kind === "file") {
|
||||||
|
return (
|
||||||
|
<FileAttachmentPill
|
||||||
|
key={attachment.attachment.id}
|
||||||
|
attachment={attachment}
|
||||||
|
index={index}
|
||||||
|
disabled={disabled}
|
||||||
|
onRemove={onRemove}
|
||||||
|
removeLabel={labels.removeFile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (composerWorkspaceAttachment.is(attachment)) {
|
if (composerWorkspaceAttachment.is(attachment)) {
|
||||||
return composerWorkspaceAttachment.renderPill({
|
return composerWorkspaceAttachment.renderPill({
|
||||||
attachment,
|
attachment,
|
||||||
@@ -642,6 +659,45 @@ function GithubAttachmentPill({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FileAttachmentPillProps {
|
||||||
|
attachment: Extract<ComposerAttachment, { kind: "file" }>;
|
||||||
|
index: number;
|
||||||
|
disabled: boolean;
|
||||||
|
onRemove: (index: number) => void;
|
||||||
|
removeLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileAttachmentPill({
|
||||||
|
attachment,
|
||||||
|
index,
|
||||||
|
disabled,
|
||||||
|
onRemove,
|
||||||
|
removeLabel,
|
||||||
|
}: FileAttachmentPillProps) {
|
||||||
|
const handleRemove = useCallback(() => {
|
||||||
|
onRemove(index);
|
||||||
|
}, [onRemove, index]);
|
||||||
|
return (
|
||||||
|
<AttachmentPill
|
||||||
|
testID="composer-file-attachment-pill"
|
||||||
|
onOpen={noopCallback}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
openAccessibilityLabel={attachment.attachment.fileName}
|
||||||
|
removeAccessibilityLabel={removeLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<View style={styles.filePillBody}>
|
||||||
|
<View style={styles.filePillIcon}>
|
||||||
|
<ThemedFileText size={ICON_SIZE.sm} uniProps={iconForegroundMutedMapping} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.filePillText} numberOfLines={1}>
|
||||||
|
{attachment.attachment.fileName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</AttachmentPill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface GithubPickerOptionProps {
|
interface GithubPickerOptionProps {
|
||||||
label: string;
|
label: string;
|
||||||
testID: string;
|
testID: string;
|
||||||
@@ -713,6 +769,8 @@ interface ComposerProps {
|
|||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
/** Callback to expose the addImages function to parent components */
|
/** Callback to expose the addImages function to parent components */
|
||||||
onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void;
|
onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||||
|
/** Callback to expose the addFiles function to parent components */
|
||||||
|
onAddFiles?: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||||
/** Callback to expose a focus function to parent components (desktop only). */
|
/** Callback to expose a focus function to parent components (desktop only). */
|
||||||
onFocusInput?: (focus: () => void) => void;
|
onFocusInput?: (focus: () => void) => void;
|
||||||
/** Optional draft context for listing commands before an agent exists. */
|
/** Optional draft context for listing commands before an agent exists. */
|
||||||
@@ -734,6 +792,8 @@ interface ComposerProps {
|
|||||||
isCompactLayout?: boolean;
|
isCompactLayout?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024;
|
||||||
|
|
||||||
const EMPTY_ARRAY: readonly QueuedMessage[] = [];
|
const EMPTY_ARRAY: readonly QueuedMessage[] = [];
|
||||||
const StableMessageInput = memo(MessageInput);
|
const StableMessageInput = memo(MessageInput);
|
||||||
|
|
||||||
@@ -898,6 +958,7 @@ function ComposerVoiceModeButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oxlint-disable-next-line complexity
|
||||||
export function Composer({
|
export function Composer({
|
||||||
agentId,
|
agentId,
|
||||||
serverId,
|
serverId,
|
||||||
@@ -921,6 +982,7 @@ export function Composer({
|
|||||||
clearDraft,
|
clearDraft,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
onAddImages,
|
onAddImages,
|
||||||
|
onAddFiles,
|
||||||
onFocusInput,
|
onFocusInput,
|
||||||
commandDraftConfig,
|
commandDraftConfig,
|
||||||
onMessageSent,
|
onMessageSent,
|
||||||
@@ -997,6 +1059,7 @@ export function Composer({
|
|||||||
});
|
});
|
||||||
const [cursorIndex, setCursorIndex] = useState(0);
|
const [cursorIndex, setCursorIndex] = useState(0);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
||||||
const [isCancellingAgent, setIsCancellingAgent] = useState(false);
|
const [isCancellingAgent, setIsCancellingAgent] = useState(false);
|
||||||
const [sendError, setSendError] = useState<string | null>(null);
|
const [sendError, setSendError] = useState<string | null>(null);
|
||||||
const [isMessageInputFocused, setIsMessageInputFocused] = useState(false);
|
const [isMessageInputFocused, setIsMessageInputFocused] = useState(false);
|
||||||
@@ -1073,6 +1136,7 @@ export function Composer({
|
|||||||
}, [userInput.length]);
|
}, [userInput.length]);
|
||||||
|
|
||||||
const { pickImages } = useImageAttachmentPicker();
|
const { pickImages } = useImageAttachmentPicker();
|
||||||
|
const { pickFiles } = useFilePicker();
|
||||||
const agentIdRef = useRef(agentId);
|
const agentIdRef = useRef(agentId);
|
||||||
const sendAgentMessageRef = useRef<
|
const sendAgentMessageRef = useRef<
|
||||||
((agentId: string, text: string, attachments: ComposerAttachment[]) => Promise<void>) | null
|
((agentId: string, text: string, attachments: ComposerAttachment[]) => Promise<void>) | null
|
||||||
@@ -1094,6 +1158,20 @@ export function Composer({
|
|||||||
onAddImages?.(addImages);
|
onAddImages?.(addImages);
|
||||||
}, [addImages, onAddImages]);
|
}, [addImages, onAddImages]);
|
||||||
|
|
||||||
|
// Expose addFiles function to parent for drag-and-drop support
|
||||||
|
const addFiles = useCallback(
|
||||||
|
(files: UserComposerAttachment[]) => {
|
||||||
|
setSelectedAttachments((prev) => [...prev, ...files]);
|
||||||
|
},
|
||||||
|
[setSelectedAttachments],
|
||||||
|
);
|
||||||
|
|
||||||
|
/* oxlint-disable react-hooks/exhaustive-deps */
|
||||||
|
useEffect(() => {
|
||||||
|
onAddFiles?.(addFiles);
|
||||||
|
}, [addFiles, onAddFiles]);
|
||||||
|
/* oxlint-enable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
const focusInput = useCallback(() => {
|
const focusInput = useCallback(() => {
|
||||||
if (isNative) return;
|
if (isNative) return;
|
||||||
focusWithRetries({
|
focusWithRetries({
|
||||||
@@ -1289,6 +1367,36 @@ export function Composer({
|
|||||||
addImages(newImages);
|
addImages(newImages);
|
||||||
}, [addImages, pickImages]);
|
}, [addImages, pickImages]);
|
||||||
|
|
||||||
|
const handlePickFile = useCallback(async () => {
|
||||||
|
if (!client) {
|
||||||
|
toastErrorRef.current(t("composer.errors.daemonClientDisconnected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const files = await pickFiles();
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const oversized = files.find((f) => f.bytes.byteLength > MAX_FILE_SIZE_BYTES);
|
||||||
|
if (oversized) {
|
||||||
|
toastErrorRef.current(
|
||||||
|
t("composer.errors.fileTooLarge", { size: "50MB", fileName: oversized.fileName }),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploadingFile(true);
|
||||||
|
const uploaded = await uploadFileAttachments({ client, files });
|
||||||
|
addFiles(uploaded);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Composer] Failed to upload file:", error);
|
||||||
|
toastErrorRef.current(
|
||||||
|
error instanceof Error ? error.message : t("composer.errors.uploadFailed"),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsUploadingFile(false);
|
||||||
|
}
|
||||||
|
}, [client, pickFiles, addFiles, t]);
|
||||||
|
|
||||||
const handleRemoveAttachment = useCallback(
|
const handleRemoveAttachment = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
githubAutoAttach.markGithubAttachmentRemoved(selectedAttachments[index]);
|
githubAutoAttach.markGithubAttachmentRemoved(selectedAttachments[index]);
|
||||||
@@ -1596,8 +1704,20 @@ export function Composer({
|
|||||||
setIsGithubPickerOpen(true);
|
setIsGithubPickerOpen(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(!isNative
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "file",
|
||||||
|
label: t("composer.attachments.addFile"),
|
||||||
|
icon: <ThemedFileText size={ICON_SIZE.md} uniProps={iconForegroundMutedMapping} />,
|
||||||
|
onSelect: () => {
|
||||||
|
void handlePickFile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
[handlePickImage, t],
|
[handlePickImage, handlePickFile, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleGithubItem = useCallback(
|
const handleToggleGithubItem = useCallback(
|
||||||
@@ -1698,6 +1818,7 @@ export function Composer({
|
|||||||
labels: {
|
labels: {
|
||||||
openImage: t("composer.attachments.openImage"),
|
openImage: t("composer.attachments.openImage"),
|
||||||
removeImage: t("composer.attachments.removeImage"),
|
removeImage: t("composer.attachments.removeImage"),
|
||||||
|
removeFile: t("composer.attachments.removeFile"),
|
||||||
openGithub: (kind: string, number: number) =>
|
openGithub: (kind: string, number: number) =>
|
||||||
t("composer.attachments.openGithub", { kind, number }),
|
t("composer.attachments.openGithub", { kind, number }),
|
||||||
removeGithub: (kind: string, number: number) =>
|
removeGithub: (kind: string, number: number) =>
|
||||||
@@ -1721,7 +1842,7 @@ export function Composer({
|
|||||||
|
|
||||||
const messageInputContainerRef = useRef<View>(null);
|
const messageInputContainerRef = useRef<View>(null);
|
||||||
|
|
||||||
const isSubmitBusy = isProcessing || isSubmitLoading;
|
const isSubmitBusy = isProcessing || isSubmitLoading || isUploadingFile;
|
||||||
const messageInputAutoFocus = autoFocus && isDesktopWebBreakpoint;
|
const messageInputAutoFocus = autoFocus && isDesktopWebBreakpoint;
|
||||||
const submitLoadingPressHandler = isAgentRunning ? handleCancelAgent : undefined;
|
const submitLoadingPressHandler = isAgentRunning ? handleCancelAgent : undefined;
|
||||||
const sendErrorNode = useMemo(
|
const sendErrorNode = useMemo(
|
||||||
@@ -1961,6 +2082,20 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
filePillBody: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
filePillIcon: {
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
filePillText: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
githubPillText: {
|
githubPillText: {
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
@@ -2030,6 +2165,7 @@ const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
|||||||
const ThemedCircleDot = withUnistyles(CircleDot);
|
const ThemedCircleDot = withUnistyles(CircleDot);
|
||||||
const ThemedAudioLines = withUnistyles(AudioLines);
|
const ThemedAudioLines = withUnistyles(AudioLines);
|
||||||
const ThemedPaperclip = withUnistyles(Paperclip);
|
const ThemedPaperclip = withUnistyles(Paperclip);
|
||||||
|
const ThemedFileText = withUnistyles(FileText);
|
||||||
const ThemedGithub = withUnistyles(Github);
|
const ThemedGithub = withUnistyles(Github);
|
||||||
|
|
||||||
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
|
|||||||
@@ -4,8 +4,19 @@ import { getDesktopHost } from "@/desktop/host";
|
|||||||
import { persistAttachmentFromBlob, persistAttachmentFromFileUri } from "@/attachments/service";
|
import { persistAttachmentFromBlob, persistAttachmentFromFileUri } from "@/attachments/service";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
|
export interface DroppedFileItem {
|
||||||
|
kind: "web-file";
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
export interface DroppedPathItem {
|
||||||
|
kind: "desktop-path";
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
export type DroppedItem = DroppedFileItem | DroppedPathItem;
|
||||||
|
|
||||||
interface UseFileDropZoneOptions {
|
interface UseFileDropZoneOptions {
|
||||||
onFilesDropped: (files: ImageAttachment[]) => void;
|
onFilesDropped: (files: ImageAttachment[]) => void;
|
||||||
|
onGenericFilesDropped?: (items: DroppedItem[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +94,24 @@ async function fileToImageAttachment(file: File): Promise<ImageAttachment> {
|
|||||||
|
|
||||||
export function useFileDropZone({
|
export function useFileDropZone({
|
||||||
onFilesDropped,
|
onFilesDropped,
|
||||||
|
onGenericFilesDropped,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: UseFileDropZoneOptions): UseFileDropZoneReturn {
|
}: UseFileDropZoneOptions): UseFileDropZoneReturn {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const containerRef = useRef<HTMLElement | null>(null);
|
const containerRef = useRef<HTMLElement | null>(null);
|
||||||
const dragCounterRef = useRef(0);
|
const dragCounterRef = useRef(0);
|
||||||
const onFilesDroppedRef = useRef(onFilesDropped);
|
const onFilesDroppedRef = useRef(onFilesDropped);
|
||||||
|
const onGenericFilesDroppedRef = useRef(onGenericFilesDropped);
|
||||||
|
|
||||||
// Keep callback ref up to date
|
// Keep callback refs up to date
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFilesDroppedRef.current = onFilesDropped;
|
onFilesDroppedRef.current = onFilesDropped;
|
||||||
}, [onFilesDropped]);
|
}, [onFilesDropped]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onGenericFilesDroppedRef.current = onGenericFilesDropped;
|
||||||
|
}, [onGenericFilesDropped]);
|
||||||
|
|
||||||
// Reset drag state when disabled changes
|
// Reset drag state when disabled changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
@@ -156,6 +173,15 @@ export function useFileDropZone({
|
|||||||
|
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
|
|
||||||
|
const items: DroppedPathItem[] = payload.paths.map((path) => ({
|
||||||
|
kind: "desktop-path",
|
||||||
|
path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (onGenericFilesDroppedRef.current && items.length > 0) {
|
||||||
|
onGenericFilesDroppedRef.current(items);
|
||||||
|
}
|
||||||
|
|
||||||
const imagePaths = payload.paths.filter(isImagePath);
|
const imagePaths = payload.paths.filter(isImagePath);
|
||||||
if (imagePaths.length === 0) {
|
if (imagePaths.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -238,6 +264,15 @@ export function useFileDropZone({
|
|||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer?.files ?? []);
|
const files = Array.from(e.dataTransfer?.files ?? []);
|
||||||
|
const genericItems: DroppedItem[] = files.map((file) => ({
|
||||||
|
kind: "web-file",
|
||||||
|
file,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (onGenericFilesDroppedRef.current && genericItems.length > 0) {
|
||||||
|
onGenericFilesDroppedRef.current(genericItems);
|
||||||
|
}
|
||||||
|
|
||||||
const imageFiles = files.filter(isImageFile);
|
const imageFiles = files.filter(isImageFile);
|
||||||
|
|
||||||
if (imageFiles.length === 0) return;
|
if (imageFiles.length === 0) return;
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
|
||||||
|
import { copyDesktopAttachmentFile } from "@/desktop/attachments/desktop-file-commands";
|
||||||
|
import { readDesktopFileBase64 } from "@/desktop/attachments/desktop-preview-url";
|
||||||
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
|
export interface PickedFile {
|
||||||
|
fileName: string;
|
||||||
|
mimeType: string;
|
||||||
|
bytes: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIME_BY_EXTENSION: Record<string, string> = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".heic": "image/heic",
|
||||||
|
".heif": "image/heif",
|
||||||
|
".avif": "image/avif",
|
||||||
|
".tif": "image/tiff",
|
||||||
|
".tiff": "image/tiff",
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
".txt": "text/plain",
|
||||||
|
".md": "text/markdown",
|
||||||
|
".json": "application/json",
|
||||||
|
".js": "text/javascript",
|
||||||
|
".ts": "text/typescript",
|
||||||
|
".tsx": "text/typescript-jsx",
|
||||||
|
".jsx": "text/javascript",
|
||||||
|
".html": "text/html",
|
||||||
|
".css": "text/css",
|
||||||
|
".xml": "text/xml",
|
||||||
|
".csv": "text/csv",
|
||||||
|
".zip": "application/zip",
|
||||||
|
".gz": "application/gzip",
|
||||||
|
".tar": "application/x-tar",
|
||||||
|
".mp3": "audio/mpeg",
|
||||||
|
".mp4": "video/mp4",
|
||||||
|
".mov": "video/quicktime",
|
||||||
|
".webm": "video/webm",
|
||||||
|
".wav": "audio/wav",
|
||||||
|
".ogg": "audio/ogg",
|
||||||
|
".doc": "application/msword",
|
||||||
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
".xls": "application/vnd.ms-excel",
|
||||||
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
".ppt": "application/vnd.ms-powerpoint",
|
||||||
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getFileExtension(path: string): string {
|
||||||
|
const normalizedPath = path.split("#", 1)[0]?.split("?", 1)[0] ?? path;
|
||||||
|
const extensionIndex = normalizedPath.lastIndexOf(".");
|
||||||
|
if (extensionIndex < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return normalizedPath.slice(extensionIndex).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMimeTypeFromPath(path: string): string {
|
||||||
|
const ext = getFileExtension(path);
|
||||||
|
return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToUint8Array(base64: string): Uint8Array {
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readDesktopFileBytes(path: string): Promise<Uint8Array> {
|
||||||
|
const { path: managedPath } = await copyDesktopAttachmentFile({
|
||||||
|
attachmentId: crypto.randomUUID(),
|
||||||
|
sourcePath: path,
|
||||||
|
extension: getFileExtension(path).slice(1) || null,
|
||||||
|
});
|
||||||
|
const base64 = await readDesktopFileBase64(managedPath);
|
||||||
|
return base64ToUint8Array(base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickFilesWithDesktopDialog(): Promise<PickedFile[] | null> {
|
||||||
|
const dialog = getDesktopHost()?.dialog;
|
||||||
|
const dialogOpen = dialog?.open;
|
||||||
|
if (typeof dialogOpen !== "function") {
|
||||||
|
throw new Error("Desktop dialog API is not available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = await dialogOpen({
|
||||||
|
directory: false,
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = Array.isArray(selection) ? selection : [selection];
|
||||||
|
if (paths.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: PickedFile[] = [];
|
||||||
|
|
||||||
|
for (const filePath of paths) {
|
||||||
|
const fileName = filePath.split("/").pop() ?? filePath.split("\\").pop() ?? filePath;
|
||||||
|
const mimeType = getMimeTypeFromPath(filePath);
|
||||||
|
|
||||||
|
// Copy into managed storage so we can read it through the existing secure IPC.
|
||||||
|
const { path: managedPath } = await copyDesktopAttachmentFile({
|
||||||
|
attachmentId: crypto.randomUUID(),
|
||||||
|
sourcePath: filePath,
|
||||||
|
extension: getFileExtension(filePath).slice(1) || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const base64 = await readDesktopFileBase64(managedPath);
|
||||||
|
const bytes = base64ToUint8Array(base64);
|
||||||
|
|
||||||
|
result.push({ fileName, mimeType, bytes });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFilesWithWebInput(): Promise<PickedFile[] | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.multiple = true;
|
||||||
|
input.style.display = "none";
|
||||||
|
|
||||||
|
input.addEventListener("change", async () => {
|
||||||
|
const files = Array.from(input.files ?? []);
|
||||||
|
if (files.length === 0) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: PickedFile[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
result.push({
|
||||||
|
fileName: file.name,
|
||||||
|
mimeType: file.type || getMimeTypeFromPath(file.name),
|
||||||
|
bytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("cancel", () => {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
// Clean up after a short delay to allow the change event to fire
|
||||||
|
setTimeout(() => {
|
||||||
|
input.remove();
|
||||||
|
}, 60_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFilePicker() {
|
||||||
|
const isPickingRef = useRef(false);
|
||||||
|
|
||||||
|
const pickFiles = useCallback(async (): Promise<PickedFile[] | null> => {
|
||||||
|
if (isPickingRef.current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
isPickingRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isWeb && isElectronRuntime()) {
|
||||||
|
return await pickFilesWithDesktopDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWeb) {
|
||||||
|
return await pickFilesWithWebInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native not supported yet
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[FilePicker] Failed to pick files:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
isPickingRef.current = false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { pickFiles };
|
||||||
|
}
|
||||||
@@ -99,12 +99,15 @@ export const ar: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "أضف صورة",
|
addImage: "أضف صورة",
|
||||||
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "أضف مشكلة أو PR",
|
addIssueOrPr: "أضف مشكلة أو PR",
|
||||||
dropImagesHere: "إسقاط الصور هنا",
|
dropImagesHere: "إسقاط الصور هنا",
|
||||||
|
dropFilesHere: "Drop files here",
|
||||||
editQueuedMessage: "تحرير الرسالة في قائمة الانتظار",
|
editQueuedMessage: "تحرير الرسالة في قائمة الانتظار",
|
||||||
sendQueuedMessageNow: "إرسال رسالة في قائمة الانتظار الآن",
|
sendQueuedMessageNow: "إرسال رسالة في قائمة الانتظار الآن",
|
||||||
openImage: "فتح مرفق الصورة",
|
openImage: "فتح مرفق الصورة",
|
||||||
removeImage: "إزالة مرفق الصورة",
|
removeImage: "إزالة مرفق الصورة",
|
||||||
|
removeFile: "Remove file attachment",
|
||||||
openGithub: "افتح{{kind}}#{{number}}",
|
openGithub: "افتح{{kind}}#{{number}}",
|
||||||
removeGithub: "إزالة{{kind}}#{{number}}",
|
removeGithub: "إزالة{{kind}}#{{number}}",
|
||||||
browserElement: "العنصر ·{{tag}}",
|
browserElement: "العنصر ·{{tag}}",
|
||||||
@@ -119,6 +122,8 @@ export const ar: TranslationResources = {
|
|||||||
noHostSelected: "لم يتم تحديد مضيف",
|
noHostSelected: "لم يتم تحديد مضيف",
|
||||||
initialPromptRequired: "مطلوب موجه الأولي",
|
initialPromptRequired: "مطلوب موجه الأولي",
|
||||||
alreadyLoading: "جارٍ التحميل بالفعل",
|
alreadyLoading: "جارٍ التحميل بالفعل",
|
||||||
|
uploadFailed: "Failed to upload file",
|
||||||
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
archiveAgent: "أرشفة الوكيل الحالي",
|
archiveAgent: "أرشفة الوكيل الحالي",
|
||||||
|
|||||||
@@ -97,12 +97,15 @@ export const en = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Add image",
|
addImage: "Add image",
|
||||||
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Add issue or PR",
|
addIssueOrPr: "Add issue or PR",
|
||||||
dropImagesHere: "Drop images here",
|
dropImagesHere: "Drop images here",
|
||||||
|
dropFilesHere: "Drop files here",
|
||||||
editQueuedMessage: "Edit queued message",
|
editQueuedMessage: "Edit queued message",
|
||||||
sendQueuedMessageNow: "Send queued message now",
|
sendQueuedMessageNow: "Send queued message now",
|
||||||
openImage: "Open image attachment",
|
openImage: "Open image attachment",
|
||||||
removeImage: "Remove image attachment",
|
removeImage: "Remove image attachment",
|
||||||
|
removeFile: "Remove file attachment",
|
||||||
openGithub: "Open {{kind}} #{{number}}",
|
openGithub: "Open {{kind}} #{{number}}",
|
||||||
removeGithub: "Remove {{kind}} #{{number}}",
|
removeGithub: "Remove {{kind}} #{{number}}",
|
||||||
browserElement: "Element · {{tag}}",
|
browserElement: "Element · {{tag}}",
|
||||||
@@ -117,6 +120,8 @@ export const en = {
|
|||||||
noHostSelected: "No host selected",
|
noHostSelected: "No host selected",
|
||||||
initialPromptRequired: "Initial prompt is required",
|
initialPromptRequired: "Initial prompt is required",
|
||||||
alreadyLoading: "Already loading",
|
alreadyLoading: "Already loading",
|
||||||
|
uploadFailed: "Failed to upload file",
|
||||||
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
archiveAgent: "Archive the current agent",
|
archiveAgent: "Archive the current agent",
|
||||||
|
|||||||
@@ -99,12 +99,15 @@ export const es: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Agregar imagen",
|
addImage: "Agregar imagen",
|
||||||
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Agregar problema oPR",
|
addIssueOrPr: "Agregar problema oPR",
|
||||||
dropImagesHere: "Suelta imágenes aquí",
|
dropImagesHere: "Suelta imágenes aquí",
|
||||||
|
dropFilesHere: "Drop files here",
|
||||||
editQueuedMessage: "Editar mensaje en cola",
|
editQueuedMessage: "Editar mensaje en cola",
|
||||||
sendQueuedMessageNow: "Enviar mensaje en cola ahora",
|
sendQueuedMessageNow: "Enviar mensaje en cola ahora",
|
||||||
openImage: "Abrir imagen adjunta",
|
openImage: "Abrir imagen adjunta",
|
||||||
removeImage: "Quitar imagen adjunta",
|
removeImage: "Quitar imagen adjunta",
|
||||||
|
removeFile: "Remove file attachment",
|
||||||
openGithub: "Abrir{{kind}}#{{number}}",
|
openGithub: "Abrir{{kind}}#{{number}}",
|
||||||
removeGithub: "Quitar{{kind}}#{{number}}",
|
removeGithub: "Quitar{{kind}}#{{number}}",
|
||||||
browserElement: "Elemento ·{{tag}}",
|
browserElement: "Elemento ·{{tag}}",
|
||||||
@@ -119,6 +122,8 @@ export const es: TranslationResources = {
|
|||||||
noHostSelected: "Ningún anfitrión seleccionado",
|
noHostSelected: "Ningún anfitrión seleccionado",
|
||||||
initialPromptRequired: "Se requiere aviso inicial",
|
initialPromptRequired: "Se requiere aviso inicial",
|
||||||
alreadyLoading: "Ya cargando",
|
alreadyLoading: "Ya cargando",
|
||||||
|
uploadFailed: "Failed to upload file",
|
||||||
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
archiveAgent: "Archivar el agente actual",
|
archiveAgent: "Archivar el agente actual",
|
||||||
|
|||||||
@@ -100,12 +100,15 @@ export const fr: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Ajouter une image",
|
addImage: "Ajouter une image",
|
||||||
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Ajouter un problème ouPR",
|
addIssueOrPr: "Ajouter un problème ouPR",
|
||||||
dropImagesHere: "Déposez des images ici",
|
dropImagesHere: "Déposez des images ici",
|
||||||
|
dropFilesHere: "Drop files here",
|
||||||
editQueuedMessage: "Modifier le message en file d'attente",
|
editQueuedMessage: "Modifier le message en file d'attente",
|
||||||
sendQueuedMessageNow: "Envoyer le message en file d'attente maintenant",
|
sendQueuedMessageNow: "Envoyer le message en file d'attente maintenant",
|
||||||
openImage: "Ouvrir la pièce jointe de l'image",
|
openImage: "Ouvrir la pièce jointe de l'image",
|
||||||
removeImage: "Supprimer l'image jointe",
|
removeImage: "Supprimer l'image jointe",
|
||||||
|
removeFile: "Remove file attachment",
|
||||||
openGithub: "Ouvrir{{kind}}#{{number}}",
|
openGithub: "Ouvrir{{kind}}#{{number}}",
|
||||||
removeGithub: "Supprimer{{kind}}#{{number}}",
|
removeGithub: "Supprimer{{kind}}#{{number}}",
|
||||||
browserElement: "Élément ·{{tag}}",
|
browserElement: "Élément ·{{tag}}",
|
||||||
@@ -120,6 +123,8 @@ export const fr: TranslationResources = {
|
|||||||
noHostSelected: "Aucun hôte sélectionné",
|
noHostSelected: "Aucun hôte sélectionné",
|
||||||
initialPromptRequired: "Une invite initiale est requise",
|
initialPromptRequired: "Une invite initiale est requise",
|
||||||
alreadyLoading: "Déjà en cours de chargement",
|
alreadyLoading: "Déjà en cours de chargement",
|
||||||
|
uploadFailed: "Failed to upload file",
|
||||||
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
archiveAgent: "Archiver l'agent actuel",
|
archiveAgent: "Archiver l'agent actuel",
|
||||||
|
|||||||
@@ -99,12 +99,15 @@ export const ru: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Добавить изображение",
|
addImage: "Добавить изображение",
|
||||||
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Добавить проблему или PR",
|
addIssueOrPr: "Добавить проблему или PR",
|
||||||
dropImagesHere: "Скиньте изображения сюда",
|
dropImagesHere: "Скиньте изображения сюда",
|
||||||
|
dropFilesHere: "Drop files here",
|
||||||
editQueuedMessage: "Изменить сообщение в очереди",
|
editQueuedMessage: "Изменить сообщение в очереди",
|
||||||
sendQueuedMessageNow: "Отправить сообщение в очереди сейчас",
|
sendQueuedMessageNow: "Отправить сообщение в очереди сейчас",
|
||||||
openImage: "Открыть прикрепленное изображение",
|
openImage: "Открыть прикрепленное изображение",
|
||||||
removeImage: "Удалить прикрепленное изображение",
|
removeImage: "Удалить прикрепленное изображение",
|
||||||
|
removeFile: "Remove file attachment",
|
||||||
openGithub: "Открыть{{kind}}#{{number}}",
|
openGithub: "Открыть{{kind}}#{{number}}",
|
||||||
removeGithub: "Удалить{{kind}}#{{number}}",
|
removeGithub: "Удалить{{kind}}#{{number}}",
|
||||||
browserElement: "Элемент ·{{tag}}",
|
browserElement: "Элемент ·{{tag}}",
|
||||||
@@ -119,6 +122,8 @@ export const ru: TranslationResources = {
|
|||||||
noHostSelected: "Хост не выбран",
|
noHostSelected: "Хост не выбран",
|
||||||
initialPromptRequired: "Требуется начальное приглашение",
|
initialPromptRequired: "Требуется начальное приглашение",
|
||||||
alreadyLoading: "Уже загружается",
|
alreadyLoading: "Уже загружается",
|
||||||
|
uploadFailed: "Failed to upload file",
|
||||||
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
archiveAgent: "Архивировать текущего агента",
|
archiveAgent: "Архивировать текущего агента",
|
||||||
|
|||||||
@@ -99,12 +99,15 @@ export const zhCN: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "添加图片",
|
addImage: "添加图片",
|
||||||
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "添加 issue 或 PR",
|
addIssueOrPr: "添加 issue 或 PR",
|
||||||
dropImagesHere: "将图片拖放到这里",
|
dropImagesHere: "将图片拖放到这里",
|
||||||
|
dropFilesHere: "Drop files here",
|
||||||
editQueuedMessage: "编辑排队消息",
|
editQueuedMessage: "编辑排队消息",
|
||||||
sendQueuedMessageNow: "立即发送排队消息",
|
sendQueuedMessageNow: "立即发送排队消息",
|
||||||
openImage: "打开图片附件",
|
openImage: "打开图片附件",
|
||||||
removeImage: "移除图片附件",
|
removeImage: "移除图片附件",
|
||||||
|
removeFile: "Remove file attachment",
|
||||||
openGithub: "打开 {{kind}} #{{number}}",
|
openGithub: "打开 {{kind}} #{{number}}",
|
||||||
removeGithub: "移除 {{kind}} #{{number}}",
|
removeGithub: "移除 {{kind}} #{{number}}",
|
||||||
browserElement: "元素 · {{tag}}",
|
browserElement: "元素 · {{tag}}",
|
||||||
@@ -119,6 +122,8 @@ export const zhCN: TranslationResources = {
|
|||||||
noHostSelected: "未选择 Host",
|
noHostSelected: "未选择 Host",
|
||||||
initialPromptRequired: "初始 prompt 必填",
|
initialPromptRequired: "初始 prompt 必填",
|
||||||
alreadyLoading: "正在加载",
|
alreadyLoading: "正在加载",
|
||||||
|
uploadFailed: "Failed to upload file",
|
||||||
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
archiveAgent: "归档当前 Agent",
|
archiveAgent: "归档当前 Agent",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import { ArchivedAgentCallout } from "@/components/archived-agent-callout";
|
|||||||
import { Composer } from "@/composer";
|
import { Composer } from "@/composer";
|
||||||
import { AgentModeControl } from "@/composer/agent-controls/mode-control";
|
import { AgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||||
import { FileDropZone } from "@/components/file-drop-zone";
|
import { FileDropZone } from "@/components/file-drop-zone";
|
||||||
|
import { uploadFileAttachments } from "@/composer/actions";
|
||||||
|
import { readDesktopFileBytes, getMimeTypeFromPath } 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 { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
||||||
import type { ImageAttachment } from "@/composer/types";
|
import type { ImageAttachment } from "@/composer/types";
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
@@ -682,6 +686,7 @@ function ChatAgentContent({
|
|||||||
const { isArchivingAgent } = useArchiveAgent();
|
const { isArchivingAgent } = useArchiveAgent();
|
||||||
const streamViewRef = useRef<AgentStreamViewHandle>(null);
|
const streamViewRef = useRef<AgentStreamViewHandle>(null);
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||||
|
const addFilesRef = useRef<((files: UserComposerAttachment[]) => void) | null>(null);
|
||||||
const clearOnAgentBlurRef = useRef<() => void>(() => {});
|
const clearOnAgentBlurRef = useRef<() => void>(() => {});
|
||||||
const wasPaneFocusedRef = useRef(isPaneFocused);
|
const wasPaneFocusedRef = useRef(isPaneFocused);
|
||||||
const reconnectToastArmedRef = useRef(false);
|
const reconnectToastArmedRef = useRef(false);
|
||||||
@@ -698,6 +703,46 @@ function ChatAgentContent({
|
|||||||
addImagesRef.current = addImages;
|
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 !item.file.type.startsWith("image/");
|
||||||
|
return !isImagePath(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);
|
||||||
|
panelToast.api.error(error instanceof Error ? error.message : "Failed to upload file");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, isConnected, panelToast.api],
|
||||||
|
);
|
||||||
|
|
||||||
const agentState = useSessionStore(
|
const agentState = useSessionStore(
|
||||||
useShallow((state) => selectChatAgentState(state, serverId, agentId)),
|
useShallow((state) => selectChatAgentState(state, serverId, agentId)),
|
||||||
);
|
);
|
||||||
@@ -1048,7 +1093,9 @@ function ChatAgentContent({
|
|||||||
streamViewRef={streamViewRef}
|
streamViewRef={streamViewRef}
|
||||||
animatedContentStyle={animatedContentStyle}
|
animatedContentStyle={animatedContentStyle}
|
||||||
handleFilesDropped={handleFilesDropped}
|
handleFilesDropped={handleFilesDropped}
|
||||||
|
handleGenericFilesDropped={handleGenericFilesDropped}
|
||||||
handleAddImagesCallback={handleAddImagesCallback}
|
handleAddImagesCallback={handleAddImagesCallback}
|
||||||
|
handleAddFilesCallback={handleAddFilesCallback}
|
||||||
handleComposerHeightChange={handleComposerHeightChange}
|
handleComposerHeightChange={handleComposerHeightChange}
|
||||||
handleMessageSent={handleMessageSent}
|
handleMessageSent={handleMessageSent}
|
||||||
showHistorySyncOverlay={showHistorySyncOverlay}
|
showHistorySyncOverlay={showHistorySyncOverlay}
|
||||||
@@ -1059,6 +1106,24 @@ function ChatAgentContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImagePath(path: string): boolean {
|
||||||
|
const ext = path.slice(path.lastIndexOf(".")).toLowerCase();
|
||||||
|
const imageExts = new Set([
|
||||||
|
".png",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".bmp",
|
||||||
|
".svg",
|
||||||
|
".heic",
|
||||||
|
".heif",
|
||||||
|
".avif",
|
||||||
|
".tif",
|
||||||
|
".tiff",
|
||||||
|
]);
|
||||||
|
return imageExts.has(ext);
|
||||||
|
}
|
||||||
function ChatAgentReadyContent({
|
function ChatAgentReadyContent({
|
||||||
serverId,
|
serverId,
|
||||||
agentId,
|
agentId,
|
||||||
@@ -1072,7 +1137,9 @@ function ChatAgentReadyContent({
|
|||||||
streamViewRef,
|
streamViewRef,
|
||||||
animatedContentStyle,
|
animatedContentStyle,
|
||||||
handleFilesDropped,
|
handleFilesDropped,
|
||||||
|
handleGenericFilesDropped,
|
||||||
handleAddImagesCallback,
|
handleAddImagesCallback,
|
||||||
|
handleAddFilesCallback,
|
||||||
handleComposerHeightChange,
|
handleComposerHeightChange,
|
||||||
handleMessageSent,
|
handleMessageSent,
|
||||||
showHistorySyncOverlay,
|
showHistorySyncOverlay,
|
||||||
@@ -1092,7 +1159,9 @@ function ChatAgentReadyContent({
|
|||||||
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
||||||
animatedContentStyle: object[];
|
animatedContentStyle: object[];
|
||||||
handleFilesDropped: (files: ImageAttachment[]) => void;
|
handleFilesDropped: (files: ImageAttachment[]) => void;
|
||||||
|
handleGenericFilesDropped: (items: DroppedItem[]) => void;
|
||||||
handleAddImagesCallback: (addImages: (images: ImageAttachment[]) => void) => void;
|
handleAddImagesCallback: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||||
|
handleAddFilesCallback: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||||
handleComposerHeightChange: (height: number) => void;
|
handleComposerHeightChange: (height: number) => void;
|
||||||
handleMessageSent: () => void;
|
handleMessageSent: () => void;
|
||||||
showHistorySyncOverlay: boolean;
|
showHistorySyncOverlay: boolean;
|
||||||
@@ -1135,6 +1204,7 @@ function ChatAgentReadyContent({
|
|||||||
onAttentionInputFocus={attentionController.clearOnInputFocus}
|
onAttentionInputFocus={attentionController.clearOnInputFocus}
|
||||||
onAttentionPromptSend={attentionController.clearOnPromptSend}
|
onAttentionPromptSend={attentionController.clearOnPromptSend}
|
||||||
onAddImages={handleAddImagesCallback}
|
onAddImages={handleAddImagesCallback}
|
||||||
|
onAddFiles={handleAddFilesCallback}
|
||||||
onComposerHeightChange={handleComposerHeightChange}
|
onComposerHeightChange={handleComposerHeightChange}
|
||||||
onMessageSent={handleMessageSent}
|
onMessageSent={handleMessageSent}
|
||||||
/>
|
/>
|
||||||
@@ -1148,7 +1218,11 @@ function ChatAgentReadyContent({
|
|||||||
return (
|
return (
|
||||||
<RewindComposerRestoreProvider text={agentInputDraft.text} setText={agentInputDraft.setText}>
|
<RewindComposerRestoreProvider text={agentInputDraft.text} setText={agentInputDraft.setText}>
|
||||||
<View style={styles.root}>
|
<View style={styles.root}>
|
||||||
<FileDropZone onFilesDropped={handleFilesDropped} disabled={isArchivingCurrentAgent}>
|
<FileDropZone
|
||||||
|
onFilesDropped={handleFilesDropped}
|
||||||
|
onGenericFilesDropped={handleGenericFilesDropped}
|
||||||
|
disabled={isArchivingCurrentAgent}
|
||||||
|
>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{contentContainer}
|
{contentContainer}
|
||||||
|
|
||||||
@@ -1258,6 +1332,7 @@ function AgentComposerSection({
|
|||||||
onAttentionInputFocus,
|
onAttentionInputFocus,
|
||||||
onAttentionPromptSend,
|
onAttentionPromptSend,
|
||||||
onAddImages,
|
onAddImages,
|
||||||
|
onAddFiles,
|
||||||
onComposerHeightChange,
|
onComposerHeightChange,
|
||||||
onMessageSent,
|
onMessageSent,
|
||||||
}: {
|
}: {
|
||||||
@@ -1272,6 +1347,7 @@ function AgentComposerSection({
|
|||||||
onAttentionInputFocus: () => void;
|
onAttentionInputFocus: () => void;
|
||||||
onAttentionPromptSend: () => void;
|
onAttentionPromptSend: () => void;
|
||||||
onAddImages: (addImages: (images: ImageAttachment[]) => void) => void;
|
onAddImages: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||||
|
onAddFiles: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||||
onComposerHeightChange: (height: number) => void;
|
onComposerHeightChange: (height: number) => void;
|
||||||
onMessageSent: () => void;
|
onMessageSent: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -1296,6 +1372,7 @@ function AgentComposerSection({
|
|||||||
onAttentionInputFocus={onAttentionInputFocus}
|
onAttentionInputFocus={onAttentionInputFocus}
|
||||||
onAttentionPromptSend={onAttentionPromptSend}
|
onAttentionPromptSend={onAttentionPromptSend}
|
||||||
onAddImages={onAddImages}
|
onAddImages={onAddImages}
|
||||||
|
onAddFiles={onAddFiles}
|
||||||
onComposerHeightChange={onComposerHeightChange}
|
onComposerHeightChange={onComposerHeightChange}
|
||||||
onMessageSent={onMessageSent}
|
onMessageSent={onMessageSent}
|
||||||
/>
|
/>
|
||||||
@@ -1312,6 +1389,7 @@ function ActiveAgentComposer({
|
|||||||
onAttentionInputFocus,
|
onAttentionInputFocus,
|
||||||
onAttentionPromptSend,
|
onAttentionPromptSend,
|
||||||
onAddImages,
|
onAddImages,
|
||||||
|
onAddFiles,
|
||||||
onComposerHeightChange,
|
onComposerHeightChange,
|
||||||
onMessageSent,
|
onMessageSent,
|
||||||
}: {
|
}: {
|
||||||
@@ -1324,6 +1402,7 @@ function ActiveAgentComposer({
|
|||||||
onAttentionInputFocus: () => void;
|
onAttentionInputFocus: () => void;
|
||||||
onAttentionPromptSend: () => void;
|
onAttentionPromptSend: () => void;
|
||||||
onAddImages: (addImages: (images: ImageAttachment[]) => void) => void;
|
onAddImages: (addImages: (images: ImageAttachment[]) => void) => void;
|
||||||
|
onAddFiles: (addFiles: (files: UserComposerAttachment[]) => void) => void;
|
||||||
onComposerHeightChange: (height: number) => void;
|
onComposerHeightChange: (height: number) => void;
|
||||||
onMessageSent: () => void;
|
onMessageSent: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -1465,6 +1544,7 @@ function ActiveAgentComposer({
|
|||||||
onAttentionInputFocus={onAttentionInputFocus}
|
onAttentionInputFocus={onAttentionInputFocus}
|
||||||
onAttentionPromptSend={onAttentionPromptSend}
|
onAttentionPromptSend={onAttentionPromptSend}
|
||||||
onAddImages={onAddImages}
|
onAddImages={onAddImages}
|
||||||
|
onAddFiles={onAddFiles}
|
||||||
onComposerHeightChange={onComposerHeightChange}
|
onComposerHeightChange={onComposerHeightChange}
|
||||||
onMessageSent={onMessageSent}
|
onMessageSent={onMessageSent}
|
||||||
onClientSlashCommand={handleClientSlashCommand}
|
onClientSlashCommand={handleClientSlashCommand}
|
||||||
|
|||||||
@@ -4064,6 +4064,7 @@ export type DictationStreamFinishMessage = z.infer<typeof DictationStreamFinishM
|
|||||||
export type DictationStreamCancelMessage = z.infer<typeof DictationStreamCancelMessageSchema>;
|
export type DictationStreamCancelMessage = z.infer<typeof DictationStreamCancelMessageSchema>;
|
||||||
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
|
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
|
||||||
export type AgentAttachment = z.infer<typeof AgentAttachmentSchema>;
|
export type AgentAttachment = z.infer<typeof AgentAttachmentSchema>;
|
||||||
|
export type UploadedFileAttachment = z.infer<typeof UploadedFileAttachmentSchema>;
|
||||||
export type FirstAgentContext = z.infer<typeof FirstAgentContextSchema>;
|
export type FirstAgentContext = z.infer<typeof FirstAgentContextSchema>;
|
||||||
export type ReviewAttachment = z.infer<typeof ReviewAttachmentSchema>;
|
export type ReviewAttachment = z.infer<typeof ReviewAttachmentSchema>;
|
||||||
export type ListProviderModelsRequestMessage = z.infer<
|
export type ListProviderModelsRequestMessage = z.infer<
|
||||||
|
|||||||
Reference in New Issue
Block a user