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:
@@ -16,6 +16,7 @@ import {
|
|||||||
expectComposerEditable,
|
expectComposerEditable,
|
||||||
expectAttachButtonDisabled,
|
expectAttachButtonDisabled,
|
||||||
fillComposerDraft,
|
fillComposerDraft,
|
||||||
|
dropFileOnComposer,
|
||||||
sendDraftToQueue,
|
sendDraftToQueue,
|
||||||
expectQueuedMessageButton,
|
expectQueuedMessageButton,
|
||||||
startRunningMockAgent,
|
startRunningMockAgent,
|
||||||
@@ -36,6 +37,11 @@ const MINIMAL_PNG = Buffer.from(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const TEST_IMAGE = { name: "test.png", mimeType: "image/png", buffer: MINIMAL_PNG };
|
const TEST_IMAGE = { name: "test.png", mimeType: "image/png", buffer: MINIMAL_PNG };
|
||||||
|
const TEST_JSON = {
|
||||||
|
name: "config.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify({ composer: "drop" })),
|
||||||
|
};
|
||||||
|
|
||||||
test.describe("Composer attachments", () => {
|
test.describe("Composer attachments", () => {
|
||||||
test("Plus menu shows image and GitHub options", async ({ page, withWorkspace }) => {
|
test("Plus menu shows image and GitHub options", async ({ page, withWorkspace }) => {
|
||||||
@@ -172,6 +178,47 @@ test.describe("Composer attachments", () => {
|
|||||||
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("dropped JSON file renders as a file attachment in active chat", async ({
|
||||||
|
page,
|
||||||
|
withWorkspace,
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
const workspace = await withWorkspace({ prefix: "attach-drop-json-" });
|
||||||
|
await workspace.navigateTo();
|
||||||
|
await clickNewChat(page);
|
||||||
|
await expectComposerVisible(page);
|
||||||
|
|
||||||
|
await dropFileOnComposer(page, TEST_JSON);
|
||||||
|
|
||||||
|
await expectAttachmentPill(page, "composer-file-attachment-pill");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dropped JSON file renders as a file attachment in New Workspace", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
const workspace = await seedWorkspace({ repoPrefix: "attach-drop-new-workspace-" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await waitForSidebarHydration(page);
|
||||||
|
await switchWorkspaceViaSidebar({
|
||||||
|
page,
|
||||||
|
serverId: getServerId(),
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await openNewWorkspaceComposer(page, {
|
||||||
|
projectKey: workspace.projectId,
|
||||||
|
projectDisplayName: workspace.projectDisplayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await dropFileOnComposer(page, TEST_JSON);
|
||||||
|
|
||||||
|
await expectAttachmentPill(page, "composer-file-attachment-pill");
|
||||||
|
} finally {
|
||||||
|
await workspace.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("clicking the X on an image pill removes it", async ({ page, withWorkspace }) => {
|
test("clicking the X on an image pill removes it", async ({ page, withWorkspace }) => {
|
||||||
test.setTimeout(60_000);
|
test.setTimeout(60_000);
|
||||||
const workspace = await withWorkspace({ prefix: "attach-remove-" });
|
const workspace = await withWorkspace({ prefix: "attach-remove-" });
|
||||||
|
|||||||
@@ -95,6 +95,33 @@ export async function expectAttachmentPill(page: Page, testID: string): Promise<
|
|||||||
await expect(page.getByTestId(testID).first()).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByTestId(testID).first()).toBeVisible({ timeout: 10_000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function dropFileOnComposer(
|
||||||
|
page: Page,
|
||||||
|
file: { name: string; mimeType: string; buffer: Buffer },
|
||||||
|
): Promise<void> {
|
||||||
|
const dataTransfer = await page.evaluateHandle(
|
||||||
|
({ name, mimeType, base64 }) => {
|
||||||
|
const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
||||||
|
const droppedFile = new File([bytes], name, { type: mimeType });
|
||||||
|
const transfer = new DataTransfer();
|
||||||
|
transfer.items.add(droppedFile);
|
||||||
|
return transfer;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: file.name,
|
||||||
|
mimeType: file.mimeType,
|
||||||
|
base64: file.buffer.toString("base64"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const composerRoot = page.getByTestId("message-input-root").filter({ visible: true }).first();
|
||||||
|
await expect(composerRoot).toBeVisible({ timeout: 10_000 });
|
||||||
|
await composerRoot.dispatchEvent("dragenter", { dataTransfer });
|
||||||
|
await composerRoot.dispatchEvent("dragover", { dataTransfer });
|
||||||
|
await composerRoot.dispatchEvent("drop", { dataTransfer });
|
||||||
|
await dataTransfer.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/** Hover to reveal the X button (hidden until hover on desktop web), then click by accessible label. */
|
/** Hover to reveal the X button (hidden until hover on desktop web), then click by accessible label. */
|
||||||
export async function removeAttachmentPill(
|
export async function removeAttachmentPill(
|
||||||
page: Page,
|
page: Page,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { getFileExtension } from "@/attachments/file-types";
|
||||||
|
import { copyDesktopAttachmentFile } from "@/desktop/attachments/desktop-file-commands";
|
||||||
|
import { readDesktopFileBase64 } from "@/desktop/attachments/desktop-preview-url";
|
||||||
|
|
||||||
|
export interface PickedFile {
|
||||||
|
fileName: string;
|
||||||
|
mimeType: string;
|
||||||
|
bytes: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) || null,
|
||||||
|
});
|
||||||
|
const base64 = await readDesktopFileBase64(managedPath);
|
||||||
|
return base64ToUint8Array(base64);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { View, Text } from "react-native";
|
import { View, Text } from "react-native";
|
||||||
|
import type { StyleProp, ViewStyle } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from "react-native-reanimated";
|
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from "react-native-reanimated";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
@@ -13,6 +14,7 @@ interface FileDropZoneProps {
|
|||||||
onFilesDropped: (files: ImageAttachment[]) => void;
|
onFilesDropped: (files: ImageAttachment[]) => void;
|
||||||
onGenericFilesDropped?: (items: DroppedItem[]) => void;
|
onGenericFilesDropped?: (items: DroppedItem[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IS_WEB = isWeb;
|
const IS_WEB = isWeb;
|
||||||
@@ -22,6 +24,7 @@ export function FileDropZone({
|
|||||||
onFilesDropped,
|
onFilesDropped,
|
||||||
onGenericFilesDropped,
|
onGenericFilesDropped,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
style,
|
||||||
}: FileDropZoneProps) {
|
}: FileDropZoneProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -46,6 +49,7 @@ export function FileDropZone({
|
|||||||
() => [styles.overlay, overlayAnimatedStyle],
|
() => [styles.overlay, overlayAnimatedStyle],
|
||||||
[overlayAnimatedStyle],
|
[overlayAnimatedStyle],
|
||||||
);
|
);
|
||||||
|
const containerStyle = useMemo(() => [styles.container, style], [style]);
|
||||||
|
|
||||||
// On non-web platforms, just render children
|
// On non-web platforms, just render children
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
@@ -56,7 +60,7 @@ export function FileDropZone({
|
|||||||
<View
|
<View
|
||||||
// Cast ref for web - View renders as div on web
|
// Cast ref for web - View renders as div on web
|
||||||
ref={containerRef as unknown as React.RefObject<View>}
|
ref={containerRef as unknown as React.RefObject<View>}
|
||||||
style={styles.container}
|
style={containerStyle}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Image, Text, View } from "react-native";
|
import { Image, Text, View } from "react-native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -26,7 +26,7 @@ import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-disp
|
|||||||
import { requireWorkspaceDirectory } from "@/utils/workspace-directory";
|
import { requireWorkspaceDirectory } from "@/utils/workspace-directory";
|
||||||
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
||||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import type { ImageAttachment, MessagePayload } from "@/composer/types";
|
import type { MessagePayload } from "@/composer/types";
|
||||||
|
|
||||||
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
@@ -363,14 +363,6 @@ export function WorkspaceSetupDialog() {
|
|||||||
const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(workspaceTitle);
|
const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(workspaceTitle);
|
||||||
const placeholderInitial = placeholderLabel.charAt(0).toUpperCase();
|
const placeholderInitial = placeholderLabel.charAt(0).toUpperCase();
|
||||||
|
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
|
||||||
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
|
||||||
addImagesRef.current?.(files);
|
|
||||||
}, []);
|
|
||||||
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
|
||||||
addImagesRef.current = addImages;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isCompact = useIsCompactFormFactor();
|
const isCompact = useIsCompactFormFactor();
|
||||||
const iconSource = useMemo(() => (iconDataUri ? { uri: iconDataUri } : null), [iconDataUri]);
|
const iconSource = useMemo(() => (iconDataUri ? { uri: iconDataUri } : null), [iconDataUri]);
|
||||||
const agentControlsWithDisabled = useMemo(
|
const agentControlsWithDisabled = useMemo(
|
||||||
@@ -427,7 +419,6 @@ export function WorkspaceSetupDialog() {
|
|||||||
snapPoints={SNAP_POINTS}
|
snapPoints={SNAP_POINTS}
|
||||||
testID="workspace-setup-dialog"
|
testID="workspace-setup-dialog"
|
||||||
desktopMaxWidth={640}
|
desktopMaxWidth={640}
|
||||||
onFilesDropped={handleFilesDropped}
|
|
||||||
>
|
>
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Composer
|
<Composer
|
||||||
@@ -447,7 +438,6 @@ export function WorkspaceSetupDialog() {
|
|||||||
commandDraftConfig={composerState?.commandDraftConfig}
|
commandDraftConfig={composerState?.commandDraftConfig}
|
||||||
agentControls={agentControlsWithDisabled}
|
agentControls={agentControlsWithDisabled}
|
||||||
inputWrapperStyle={styles.composerInputWrapper}
|
inputWrapperStyle={styles.composerInputWrapper}
|
||||||
onAddImages={handleAddImagesCallback}
|
|
||||||
footer={composerFooter}
|
footer={composerFooter}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { droppedItemsToPickedFiles } from "./drop";
|
||||||
|
|
||||||
|
describe("composer dropped attachments", () => {
|
||||||
|
it("turns non-image browser files into picked files and leaves raster images for image handling", async () => {
|
||||||
|
const jsonFile = new File([JSON.stringify({ ok: true })], "config.json", {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
const imageFile = new File([new Uint8Array([0])], "screen.png", { type: "image/png" });
|
||||||
|
|
||||||
|
const files = await droppedItemsToPickedFiles([
|
||||||
|
{ kind: "web-file", file: jsonFile },
|
||||||
|
{ kind: "web-file", file: imageFile },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(files).toEqual([
|
||||||
|
{
|
||||||
|
fileName: "config.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
bytes: new Uint8Array(await jsonFile.arrayBuffer()),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("turns non-image desktop paths into picked files and leaves raster images for image handling", async () => {
|
||||||
|
const windowsPath = "C:\\Users\\alice\\config.json";
|
||||||
|
const posixPath = "/Users/alice/notes/readme.txt";
|
||||||
|
const imagePath = "C:\\Users\\alice\\screen.png";
|
||||||
|
const bytesByPath = new Map([
|
||||||
|
[windowsPath, new Uint8Array([1, 2, 3])],
|
||||||
|
[posixPath, new Uint8Array([4, 5])],
|
||||||
|
]);
|
||||||
|
const readPaths: string[] = [];
|
||||||
|
|
||||||
|
const files = await droppedItemsToPickedFiles(
|
||||||
|
[
|
||||||
|
{ kind: "desktop-path", path: windowsPath },
|
||||||
|
{ kind: "desktop-path", path: imagePath },
|
||||||
|
{ kind: "desktop-path", path: posixPath },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
readDesktopFileBytes: async (path) => {
|
||||||
|
readPaths.push(path);
|
||||||
|
const bytes = bytesByPath.get(path);
|
||||||
|
if (!bytes) {
|
||||||
|
throw new Error(`Unexpected desktop read: ${path}`);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readPaths).toEqual([windowsPath, posixPath]);
|
||||||
|
expect(files).toEqual([
|
||||||
|
{
|
||||||
|
fileName: "config.json",
|
||||||
|
mimeType: "application/octet-stream",
|
||||||
|
bytes: new Uint8Array([1, 2, 3]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: "readme.txt",
|
||||||
|
mimeType: "application/octet-stream",
|
||||||
|
bytes: new Uint8Array([4, 5]),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
getMimeTypeFromPath,
|
||||||
|
isRasterImageFile,
|
||||||
|
isRasterImagePath,
|
||||||
|
} from "@/attachments/file-types";
|
||||||
|
import { readDesktopFileBytes, type PickedFile } from "@/attachments/picked-file";
|
||||||
|
import type { DroppedItem } from "@/hooks/use-file-drop-zone";
|
||||||
|
|
||||||
|
interface DroppedAttachmentsRuntime {
|
||||||
|
readDesktopFileBytes(path: string): Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultRuntime: DroppedAttachmentsRuntime = {
|
||||||
|
readDesktopFileBytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
function fileNameFromPath(path: string): string {
|
||||||
|
const segments = path.split(/[/\\]/);
|
||||||
|
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
||||||
|
const segment = segments[index];
|
||||||
|
if (segment) {
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function droppedItemsToPickedFiles(
|
||||||
|
items: DroppedItem[],
|
||||||
|
runtime: DroppedAttachmentsRuntime = defaultRuntime,
|
||||||
|
): Promise<PickedFile[]> {
|
||||||
|
const files: PickedFile[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.kind === "web-file") {
|
||||||
|
if (isRasterImageFile(item.file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
files.push({
|
||||||
|
fileName: item.file.name,
|
||||||
|
mimeType: item.file.type || getMimeTypeFromPath(item.file.name),
|
||||||
|
bytes: new Uint8Array(await item.file.arrayBuffer()),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRasterImagePath(item.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
files.push({
|
||||||
|
fileName: fileNameFromPath(item.path),
|
||||||
|
mimeType: getMimeTypeFromPath(item.path),
|
||||||
|
bytes: await runtime.readDesktopFileBytes(item.path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
@@ -10,10 +10,8 @@ import invariant from "tiny-invariant";
|
|||||||
import { Composer } from "@/composer";
|
import { Composer } from "@/composer";
|
||||||
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||||
import { ComposerImportPill } from "@/composer/draft/import-pill";
|
import { ComposerImportPill } from "@/composer/draft/import-pill";
|
||||||
import { FileDropZone } from "@/components/file-drop-zone";
|
|
||||||
import { AgentStreamView } from "@/agent-stream/view";
|
import { AgentStreamView } from "@/agent-stream/view";
|
||||||
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
|
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
|
||||||
import type { ImageAttachment } from "@/composer/types";
|
|
||||||
import { useAgentInputDraft } from "@/composer/draft/input-draft";
|
import { useAgentInputDraft } from "@/composer/draft/input-draft";
|
||||||
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
|
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
|
||||||
import { useDraftAgentCreateFlow, type DraftCreateAttempt } from "@/composer/draft/create-flow";
|
import { useDraftAgentCreateFlow, type DraftCreateAttempt } from "@/composer/draft/create-flow";
|
||||||
@@ -332,7 +330,6 @@ export function WorkspaceDraftAgentTab({
|
|||||||
initialSetup: draftSetup,
|
initialSetup: draftSetup,
|
||||||
});
|
});
|
||||||
const onlineServerIds = resolveOnlineServerIds({ isConnected, serverId });
|
const onlineServerIds = resolveOnlineServerIds({ isConnected, serverId });
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
|
||||||
const draftStoreKey = useMemo(
|
const draftStoreKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildDraftStoreKey({
|
buildDraftStoreKey({
|
||||||
@@ -540,14 +537,6 @@ export function WorkspaceDraftAgentTab({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
|
||||||
addImagesRef.current?.(files);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
|
||||||
addImagesRef.current = addImages;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const focusInputRef = useRef<(() => void) | null>(null);
|
const focusInputRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const handleFocusInputCallback = useCallback((focus: () => void) => {
|
const handleFocusInputCallback = useCallback((focus: () => void) => {
|
||||||
@@ -655,71 +644,65 @@ export function WorkspaceDraftAgentTab({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileDropZone onFilesDropped={handleFilesDropped}>
|
<View style={styles.container}>
|
||||||
<View style={styles.container}>
|
<View style={styles.contentContainer}>
|
||||||
<View style={styles.contentContainer}>
|
{isSubmitting && draftAgent ? (
|
||||||
{isSubmitting && draftAgent ? (
|
<View style={styles.streamContainer}>
|
||||||
<View style={styles.streamContainer}>
|
<AgentStreamView
|
||||||
<AgentStreamView
|
agentId={tabId}
|
||||||
agentId={tabId}
|
serverId={serverId}
|
||||||
serverId={serverId}
|
agent={draftAgent}
|
||||||
agent={draftAgent}
|
streamItems={optimisticStreamItems}
|
||||||
streamItems={optimisticStreamItems}
|
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
||||||
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
/>
|
||||||
/>
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
|
||||||
|
<View style={styles.configSection}>
|
||||||
|
{formErrorMessage ? (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>{formErrorMessage}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
</ScrollView>
|
||||||
<ScrollView
|
)}
|
||||||
style={styles.scrollView}
|
|
||||||
contentContainerStyle={styles.configScrollContent}
|
|
||||||
>
|
|
||||||
<View style={styles.configSection}>
|
|
||||||
{formErrorMessage ? (
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<Text style={styles.errorText}>{formErrorMessage}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ReanimatedAnimated.View style={inputAreaWrapperStyle} onLayout={onInputAreaLayout}>
|
|
||||||
{importPillPress ? (
|
|
||||||
<View style={styles.importPillRow}>
|
|
||||||
<View style={styles.importPillContent}>
|
|
||||||
<ComposerImportPill onPress={importPillPress} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
<Composer
|
|
||||||
agentId={tabId}
|
|
||||||
serverId={serverId}
|
|
||||||
externalKeyboardShift
|
|
||||||
isPaneFocused={isPaneFocused}
|
|
||||||
onSubmitMessage={handleCreateFromInput}
|
|
||||||
isSubmitLoading={isSubmitting}
|
|
||||||
blurOnSubmit={true}
|
|
||||||
value={draftInput.text}
|
|
||||||
onChangeText={draftInput.setText}
|
|
||||||
attachments={draftInput.attachments}
|
|
||||||
workspaceAttachments={workspaceAttachments}
|
|
||||||
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
|
|
||||||
onChangeAttachments={draftInput.setAttachments}
|
|
||||||
cwd={composerState.workingDir}
|
|
||||||
clearDraft={draftInput.clear}
|
|
||||||
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
|
|
||||||
onAddImages={handleAddImagesCallback}
|
|
||||||
onFocusInput={handleFocusInputCallback}
|
|
||||||
commandDraftConfig={composerState.commandDraftConfig}
|
|
||||||
agentControls={composerAgentControls}
|
|
||||||
footer={composerFooter}
|
|
||||||
isCompactLayout={isCompactComposerLayout}
|
|
||||||
/>
|
|
||||||
</ReanimatedAnimated.View>
|
|
||||||
</View>
|
</View>
|
||||||
</FileDropZone>
|
|
||||||
|
<ReanimatedAnimated.View style={inputAreaWrapperStyle} onLayout={onInputAreaLayout}>
|
||||||
|
{importPillPress ? (
|
||||||
|
<View style={styles.importPillRow}>
|
||||||
|
<View style={styles.importPillContent}>
|
||||||
|
<ComposerImportPill onPress={importPillPress} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<Composer
|
||||||
|
agentId={tabId}
|
||||||
|
serverId={serverId}
|
||||||
|
externalKeyboardShift
|
||||||
|
isPaneFocused={isPaneFocused}
|
||||||
|
onSubmitMessage={handleCreateFromInput}
|
||||||
|
isSubmitLoading={isSubmitting}
|
||||||
|
blurOnSubmit={true}
|
||||||
|
value={draftInput.text}
|
||||||
|
onChangeText={draftInput.setText}
|
||||||
|
attachments={draftInput.attachments}
|
||||||
|
workspaceAttachments={workspaceAttachments}
|
||||||
|
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
|
||||||
|
onChangeAttachments={draftInput.setAttachments}
|
||||||
|
cwd={composerState.workingDir}
|
||||||
|
clearDraft={draftInput.clear}
|
||||||
|
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
|
||||||
|
onFocusInput={handleFocusInputCallback}
|
||||||
|
commandDraftConfig={composerState.commandDraftConfig}
|
||||||
|
agentControls={composerAgentControls}
|
||||||
|
footer={composerFooter}
|
||||||
|
isCompactLayout={isCompactComposerLayout}
|
||||||
|
/>
|
||||||
|
</ReanimatedAnimated.View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+147
-111
@@ -43,6 +43,8 @@ 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 { useFilePicker } from "@/hooks/use-file-picker";
|
||||||
|
import { FileDropZone } from "@/components/file-drop-zone";
|
||||||
|
import type { DroppedItem } from "@/hooks/use-file-drop-zone";
|
||||||
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";
|
||||||
@@ -99,7 +101,9 @@ import type {
|
|||||||
UserComposerAttachment,
|
UserComposerAttachment,
|
||||||
WorkspaceComposerAttachment,
|
WorkspaceComposerAttachment,
|
||||||
} from "@/attachments/types";
|
} from "@/attachments/types";
|
||||||
|
import type { PickedFile } from "@/attachments/picked-file";
|
||||||
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
|
import { composerWorkspaceAttachment } from "@/composer/attachments/workspace";
|
||||||
|
import { droppedItemsToPickedFiles } from "@/composer/attachments/drop";
|
||||||
import { getFileTypeLabel } from "@/attachments/file-types";
|
import { getFileTypeLabel } from "@/attachments/file-types";
|
||||||
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
||||||
import { AttachmentLabel, AttachmentPill, AttachmentThumbnail } from "@/components/attachment-pill";
|
import { AttachmentLabel, AttachmentPill, AttachmentThumbnail } from "@/components/attachment-pill";
|
||||||
@@ -762,10 +766,6 @@ interface ComposerProps {
|
|||||||
clearDraft: (lifecycle: "sent" | "abandoned") => void;
|
clearDraft: (lifecycle: "sent" | "abandoned") => void;
|
||||||
/** When true, auto-focuses the text input on web. */
|
/** When true, auto-focuses the text input on web. */
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
/** Callback to expose the addImages function to parent components */
|
|
||||||
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. */
|
||||||
@@ -977,8 +977,6 @@ export function Composer({
|
|||||||
cwd,
|
cwd,
|
||||||
clearDraft,
|
clearDraft,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
onAddImages,
|
|
||||||
onAddFiles,
|
|
||||||
onFocusInput,
|
onFocusInput,
|
||||||
commandDraftConfig,
|
commandDraftConfig,
|
||||||
onMessageSent,
|
onMessageSent,
|
||||||
@@ -1139,7 +1137,6 @@ export function Composer({
|
|||||||
>(null);
|
>(null);
|
||||||
const onSubmitMessageRef = useRef(onSubmitMessage);
|
const onSubmitMessageRef = useRef(onSubmitMessage);
|
||||||
|
|
||||||
// Expose addImages function to parent for drag-and-drop support
|
|
||||||
const addImages = useCallback(
|
const addImages = useCallback(
|
||||||
(images: ImageAttachment[]) => {
|
(images: ImageAttachment[]) => {
|
||||||
setSelectedAttachments((prev) => [
|
setSelectedAttachments((prev) => [
|
||||||
@@ -1150,11 +1147,6 @@ export function Composer({
|
|||||||
[setSelectedAttachments],
|
[setSelectedAttachments],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onAddImages?.(addImages);
|
|
||||||
}, [addImages, onAddImages]);
|
|
||||||
|
|
||||||
// Expose addFiles function to parent for drag-and-drop support
|
|
||||||
const addFiles = useCallback(
|
const addFiles = useCallback(
|
||||||
(files: UserComposerAttachment[]) => {
|
(files: UserComposerAttachment[]) => {
|
||||||
setSelectedAttachments((prev) => [...prev, ...files]);
|
setSelectedAttachments((prev) => [...prev, ...files]);
|
||||||
@@ -1162,12 +1154,6 @@ export function Composer({
|
|||||||
[setSelectedAttachments],
|
[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({
|
||||||
@@ -1363,14 +1349,13 @@ export function Composer({
|
|||||||
addImages(newImages);
|
addImages(newImages);
|
||||||
}, [addImages, pickImages]);
|
}, [addImages, pickImages]);
|
||||||
|
|
||||||
const handlePickFile = useCallback(async () => {
|
const uploadPickedFiles = useCallback(
|
||||||
if (!client) {
|
async (files: PickedFile[]) => {
|
||||||
toastErrorRef.current(t("composer.errors.daemonClientDisconnected"));
|
if (files.length === 0) return;
|
||||||
return;
|
if (!client) {
|
||||||
}
|
toastErrorRef.current(t("composer.errors.daemonClientDisconnected"));
|
||||||
try {
|
return;
|
||||||
const files = await pickFiles();
|
}
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
|
||||||
const oversized = files.find((f) => f.bytes.byteLength > MAX_FILE_SIZE_BYTES);
|
const oversized = files.find((f) => f.bytes.byteLength > MAX_FILE_SIZE_BYTES);
|
||||||
if (oversized) {
|
if (oversized) {
|
||||||
@@ -1381,17 +1366,57 @@ export function Composer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsUploadingFile(true);
|
setIsUploadingFile(true);
|
||||||
const uploaded = await uploadFileAttachments({ client, files });
|
try {
|
||||||
addFiles(uploaded);
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addFiles, client, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePickFile = useCallback(async () => {
|
||||||
|
if (!client) {
|
||||||
|
toastErrorRef.current(t("composer.errors.daemonClientDisconnected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const files = await pickFiles();
|
||||||
|
if (!files) return;
|
||||||
|
await uploadPickedFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Composer] Failed to upload file:", error);
|
console.error("[Composer] Failed to upload file:", error);
|
||||||
toastErrorRef.current(
|
toastErrorRef.current(
|
||||||
error instanceof Error ? error.message : t("composer.errors.uploadFailed"),
|
error instanceof Error ? error.message : t("composer.errors.uploadFailed"),
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
setIsUploadingFile(false);
|
|
||||||
}
|
}
|
||||||
}, [client, pickFiles, addFiles, t]);
|
}, [client, pickFiles, t, uploadPickedFiles]);
|
||||||
|
|
||||||
|
const handleGenericFilesDropped = useCallback(
|
||||||
|
async (items: DroppedItem[]) => {
|
||||||
|
try {
|
||||||
|
const files = await droppedItemsToPickedFiles(items);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
if (!client || !isConnected) {
|
||||||
|
toastErrorRef.current(t("composer.errors.daemonClientDisconnected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await uploadPickedFiles(files);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Composer] Failed to upload dropped files:", error);
|
||||||
|
toastErrorRef.current(
|
||||||
|
error instanceof Error ? error.message : t("composer.errors.uploadFailed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, isConnected, t, uploadPickedFiles],
|
||||||
|
);
|
||||||
|
|
||||||
const handleRemoveAttachment = useCallback(
|
const handleRemoveAttachment = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
@@ -1869,90 +1894,97 @@ export function Composer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerKeyboardScopeProvider isActiveComposer={isPaneFocused}>
|
<ComposerKeyboardScopeProvider isActiveComposer={isPaneFocused}>
|
||||||
<Animated.View style={composerContainerStyle}>
|
<FileDropZone
|
||||||
<AttachmentLightbox metadata={lightboxMetadata} onClose={handleLightboxClose} />
|
onFilesDropped={addImages}
|
||||||
{/* Input area */}
|
onGenericFilesDropped={handleGenericFilesDropped}
|
||||||
<View style={inputAreaContainerStyle}>
|
disabled={isSubmitBusy}
|
||||||
<View style={styles.inputAreaContent}>
|
style={styles.dropZone}
|
||||||
{queueList}
|
>
|
||||||
{sendErrorNode}
|
<Animated.View style={composerContainerStyle}>
|
||||||
|
<AttachmentLightbox metadata={lightboxMetadata} onClose={handleLightboxClose} />
|
||||||
|
{/* Input area */}
|
||||||
|
<View style={inputAreaContainerStyle}>
|
||||||
|
<View style={styles.inputAreaContent}>
|
||||||
|
{queueList}
|
||||||
|
{sendErrorNode}
|
||||||
|
|
||||||
<View ref={messageInputContainerRef} style={styles.messageInputContainer}>
|
<View ref={messageInputContainerRef} style={styles.messageInputContainer}>
|
||||||
<AutocompletePopover
|
<AutocompletePopover
|
||||||
visible={autocompleteVisible}
|
visible={autocompleteVisible}
|
||||||
anchorRef={messageInputContainerRef}
|
anchorRef={messageInputContainerRef}
|
||||||
options={autocomplete.options}
|
options={autocomplete.options}
|
||||||
selectedIndex={autocomplete.selectedIndex}
|
selectedIndex={autocomplete.selectedIndex}
|
||||||
onSelect={autocomplete.onSelectOption}
|
onSelect={autocomplete.onSelectOption}
|
||||||
isLoading={autocomplete.isLoading}
|
isLoading={autocomplete.isLoading}
|
||||||
errorMessage={autocomplete.errorMessage}
|
errorMessage={autocomplete.errorMessage}
|
||||||
loadingText={autocomplete.loadingText}
|
loadingText={autocomplete.loadingText}
|
||||||
emptyText={autocomplete.emptyText}
|
emptyText={autocomplete.emptyText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MessageInput handles everything: text, dictation, attachments, all buttons */}
|
{/* MessageInput handles everything: text, dictation, attachments, all buttons */}
|
||||||
<StableMessageInput
|
<StableMessageInput
|
||||||
ref={messageInputRef}
|
ref={messageInputRef}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onChangeText={setUserInput}
|
onChangeText={setUserInput}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
hasExternalContent={hasExternalContent}
|
hasExternalContent={hasExternalContent}
|
||||||
allowEmptySubmit={allowEmptySubmit}
|
allowEmptySubmit={allowEmptySubmit}
|
||||||
submitButtonAccessibilityLabel={submitButtonAccessibilityLabel}
|
submitButtonAccessibilityLabel={submitButtonAccessibilityLabel}
|
||||||
submitButtonTestID={submitButtonTestID}
|
submitButtonTestID={submitButtonTestID}
|
||||||
submitIcon={submitIcon}
|
submitIcon={submitIcon}
|
||||||
isSubmitDisabled={isSubmitBusy}
|
isSubmitDisabled={isSubmitBusy}
|
||||||
isSubmitLoading={isSubmitBusy}
|
isSubmitLoading={isSubmitBusy}
|
||||||
preserveHeightOnSubmit={submitBehavior === "preserve-and-lock"}
|
preserveHeightOnSubmit={submitBehavior === "preserve-and-lock"}
|
||||||
attachments={selectedAttachments}
|
attachments={selectedAttachments}
|
||||||
cwd={cwd}
|
cwd={cwd}
|
||||||
attachmentMenuItems={attachmentMenuItems}
|
attachmentMenuItems={attachmentMenuItems}
|
||||||
onAttachButtonRef={handleAttachButtonRef}
|
onAttachButtonRef={handleAttachButtonRef}
|
||||||
onAddImages={addImages}
|
onAddImages={addImages}
|
||||||
client={client}
|
client={client}
|
||||||
isReadyForDictation={isDictationReady}
|
isReadyForDictation={isDictationReady}
|
||||||
placeholder={messagePlaceholder}
|
placeholder={messagePlaceholder}
|
||||||
autoFocus={messageInputAutoFocus}
|
autoFocus={messageInputAutoFocus}
|
||||||
autoFocusKey={`${serverId}:${agentId}`}
|
autoFocusKey={`${serverId}:${agentId}`}
|
||||||
disabled={isSubmitLoading}
|
disabled={isSubmitLoading}
|
||||||
isPaneFocused={isPaneFocused}
|
isPaneFocused={isPaneFocused}
|
||||||
leftContent={leftContent}
|
leftContent={leftContent}
|
||||||
beforeVoiceContent={beforeVoiceContent}
|
beforeVoiceContent={beforeVoiceContent}
|
||||||
rightContent={rightContent}
|
rightContent={rightContent}
|
||||||
voiceServerId={serverId}
|
voiceServerId={serverId}
|
||||||
voiceAgentId={agentId}
|
voiceAgentId={agentId}
|
||||||
isAgentRunning={isAgentRunning}
|
isAgentRunning={isAgentRunning}
|
||||||
defaultSendBehavior={appSettings.sendBehavior}
|
defaultSendBehavior={appSettings.sendBehavior}
|
||||||
onQueue={handleQueue}
|
onQueue={handleQueue}
|
||||||
onSubmitLoadingPress={submitLoadingPressHandler}
|
onSubmitLoadingPress={submitLoadingPressHandler}
|
||||||
onKeyPress={handleCommandKeyPress}
|
onKeyPress={handleCommandKeyPress}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
onFocusChange={handleFocusChange}
|
onFocusChange={handleFocusChange}
|
||||||
onHeightChange={onComposerHeightChange}
|
onHeightChange={onComposerHeightChange}
|
||||||
inputWrapperStyle={inputWrapperStyle}
|
inputWrapperStyle={inputWrapperStyle}
|
||||||
attachmentSlot={attachmentTray}
|
attachmentSlot={attachmentTray}
|
||||||
/>
|
/>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={githubSearchOptions}
|
options={githubSearchOptions}
|
||||||
value=""
|
value=""
|
||||||
onSelect={noop}
|
onSelect={noop}
|
||||||
keepOpenOnSelect
|
keepOpenOnSelect
|
||||||
searchable
|
searchable
|
||||||
searchPlaceholder={t("composer.github.searchPlaceholder")}
|
searchPlaceholder={t("composer.github.searchPlaceholder")}
|
||||||
title={t("composer.github.title")}
|
title={t("composer.github.title")}
|
||||||
open={isGithubPickerOpen}
|
open={isGithubPickerOpen}
|
||||||
onOpenChange={handleGithubPickerOpenChange}
|
onOpenChange={handleGithubPickerOpenChange}
|
||||||
onSearchQueryChange={setGithubSearchQuery}
|
onSearchQueryChange={setGithubSearchQuery}
|
||||||
desktopPlacement="top-start"
|
desktopPlacement="top-start"
|
||||||
anchorRef={attachButtonRef}
|
anchorRef={attachButtonRef}
|
||||||
emptyText={githubEmptyText}
|
emptyText={githubEmptyText}
|
||||||
renderOption={renderGithubPickerOption}
|
renderOption={renderGithubPickerOption}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
{renderComposerFooter(footer, footerInlineContent)}
|
||||||
{renderComposerFooter(footer, footerInlineContent)}
|
</Animated.View>
|
||||||
</Animated.View>
|
</FileDropZone>
|
||||||
</ComposerKeyboardScopeProvider>
|
</ComposerKeyboardScopeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1962,6 +1994,10 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
|
dropZone: {
|
||||||
|
flex: 0,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
borderSeparator: {
|
borderSeparator: {
|
||||||
height: theme.borderWidth[1],
|
height: theme.borderWidth[1],
|
||||||
backgroundColor: theme.colors.border,
|
backgroundColor: theme.colors.border,
|
||||||
|
|||||||
@@ -2,35 +2,9 @@ import { useCallback, useRef } from "react";
|
|||||||
import * as DocumentPicker from "expo-document-picker";
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
import { File } from "expo-file-system";
|
import { File } from "expo-file-system";
|
||||||
import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
|
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";
|
import { isWeb } from "@/constants/platform";
|
||||||
import { getFileExtension, getMimeTypeFromPath } from "@/attachments/file-types";
|
import { getMimeTypeFromPath } from "@/attachments/file-types";
|
||||||
|
import { readDesktopFileBytes, type PickedFile } from "@/attachments/picked-file";
|
||||||
export interface PickedFile {
|
|
||||||
fileName: string;
|
|
||||||
mimeType: string;
|
|
||||||
bytes: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) || null,
|
|
||||||
});
|
|
||||||
const base64 = await readDesktopFileBase64(managedPath);
|
|
||||||
return base64ToUint8Array(base64);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pickFilesWithDesktopDialog(): Promise<PickedFile[] | null> {
|
async function pickFilesWithDesktopDialog(): Promise<PickedFile[] | null> {
|
||||||
const dialog = getDesktopHost()?.dialog;
|
const dialog = getDesktopHost()?.dialog;
|
||||||
@@ -58,16 +32,7 @@ async function pickFilesWithDesktopDialog(): Promise<PickedFile[] | null> {
|
|||||||
for (const filePath of paths) {
|
for (const filePath of paths) {
|
||||||
const fileName = filePath.split("/").pop() ?? filePath.split("\\").pop() ?? filePath;
|
const fileName = filePath.split("/").pop() ?? filePath.split("\\").pop() ?? filePath;
|
||||||
const mimeType = getMimeTypeFromPath(filePath);
|
const mimeType = getMimeTypeFromPath(filePath);
|
||||||
|
const bytes = await readDesktopFileBytes(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) || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const base64 = await readDesktopFileBase64(managedPath);
|
|
||||||
const bytes = base64ToUint8Array(base64);
|
|
||||||
|
|
||||||
result.push({ fileName, mimeType, bytes });
|
result.push({ fileName, mimeType, bytes });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,18 +14,7 @@ import { AgentStreamView, type AgentStreamViewHandle } from "@/agent-stream/view
|
|||||||
import { ArchivedAgentCallout } from "@/components/archived-agent-callout";
|
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 { 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 { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
||||||
import type { ImageAttachment } from "@/composer/types";
|
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import {
|
import {
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
@@ -706,8 +695,6 @@ function ChatAgentContent({
|
|||||||
const { api: toastApi, toast: toastState, dismiss: dismissToast } = useToastHost();
|
const { api: toastApi, toast: toastState, dismiss: dismissToast } = useToastHost();
|
||||||
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 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);
|
||||||
@@ -716,54 +703,6 @@ function ChatAgentContent({
|
|||||||
routeKey: string;
|
routeKey: string;
|
||||||
reason: "initial-entry" | "resume";
|
reason: "initial-entry" | "resume";
|
||||||
} | null>(null);
|
} | 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(
|
const agentState = useSessionStore(
|
||||||
useShallow((state) => selectChatAgentState(state, serverId, agentId)),
|
useShallow((state) => selectChatAgentState(state, serverId, agentId)),
|
||||||
);
|
);
|
||||||
@@ -1121,10 +1060,6 @@ function ChatAgentContent({
|
|||||||
dismiss={dismissToast}
|
dismiss={dismissToast}
|
||||||
streamViewRef={streamViewRef}
|
streamViewRef={streamViewRef}
|
||||||
animatedContentStyle={animatedContentStyle}
|
animatedContentStyle={animatedContentStyle}
|
||||||
handleFilesDropped={handleFilesDropped}
|
|
||||||
handleGenericFilesDropped={handleGenericFilesDropped}
|
|
||||||
handleAddImagesCallback={handleAddImagesCallback}
|
|
||||||
handleAddFilesCallback={handleAddFilesCallback}
|
|
||||||
handleComposerHeightChange={handleComposerHeightChange}
|
handleComposerHeightChange={handleComposerHeightChange}
|
||||||
handleMessageSent={handleMessageSent}
|
handleMessageSent={handleMessageSent}
|
||||||
showHistorySyncOverlay={showHistorySyncOverlay}
|
showHistorySyncOverlay={showHistorySyncOverlay}
|
||||||
@@ -1150,10 +1085,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
dismiss,
|
dismiss,
|
||||||
streamViewRef,
|
streamViewRef,
|
||||||
animatedContentStyle,
|
animatedContentStyle,
|
||||||
handleFilesDropped,
|
|
||||||
handleGenericFilesDropped,
|
|
||||||
handleAddImagesCallback,
|
|
||||||
handleAddFilesCallback,
|
|
||||||
handleComposerHeightChange,
|
handleComposerHeightChange,
|
||||||
handleMessageSent,
|
handleMessageSent,
|
||||||
showHistorySyncOverlay,
|
showHistorySyncOverlay,
|
||||||
@@ -1175,10 +1106,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
dismiss: () => void;
|
dismiss: () => void;
|
||||||
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
||||||
animatedContentStyle: object[];
|
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;
|
handleComposerHeightChange: (height: number) => void;
|
||||||
handleMessageSent: () => void;
|
handleMessageSent: () => void;
|
||||||
showHistorySyncOverlay: boolean;
|
showHistorySyncOverlay: boolean;
|
||||||
@@ -1237,8 +1164,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
agentInputDraft={agentInputDraft}
|
agentInputDraft={agentInputDraft}
|
||||||
onAttentionInputFocus={onAttentionInputFocus}
|
onAttentionInputFocus={onAttentionInputFocus}
|
||||||
onAttentionPromptSend={onAttentionPromptSend}
|
onAttentionPromptSend={onAttentionPromptSend}
|
||||||
onAddImages={handleAddImagesCallback}
|
|
||||||
onAddFiles={handleAddFilesCallback}
|
|
||||||
onComposerHeightChange={handleComposerHeightChange}
|
onComposerHeightChange={handleComposerHeightChange}
|
||||||
onMessageSent={handleMessageSent}
|
onMessageSent={handleMessageSent}
|
||||||
/>
|
/>
|
||||||
@@ -1252,25 +1177,19 @@ const ChatAgentReadyContent = memo(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
|
<View style={styles.container}>
|
||||||
onFilesDropped={handleFilesDropped}
|
{contentContainer}
|
||||||
onGenericFilesDropped={handleGenericFilesDropped}
|
|
||||||
disabled={isArchivingCurrentAgent}
|
|
||||||
>
|
|
||||||
<View style={styles.container}>
|
|
||||||
{contentContainer}
|
|
||||||
|
|
||||||
{composerSection}
|
{composerSection}
|
||||||
|
|
||||||
{showHistorySyncOverlay ? (
|
{showHistorySyncOverlay ? (
|
||||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ToastViewport toast={toast} onDismiss={dismiss} placement="panel" />
|
<ToastViewport toast={toast} onDismiss={dismiss} placement="panel" />
|
||||||
</View>
|
</View>
|
||||||
</FileDropZone>
|
|
||||||
|
|
||||||
{isArchivingCurrentAgent ? (
|
{isArchivingCurrentAgent ? (
|
||||||
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
||||||
@@ -1361,8 +1280,6 @@ const AgentComposerSection = memo(function AgentComposerSection({
|
|||||||
agentInputDraft,
|
agentInputDraft,
|
||||||
onAttentionInputFocus,
|
onAttentionInputFocus,
|
||||||
onAttentionPromptSend,
|
onAttentionPromptSend,
|
||||||
onAddImages,
|
|
||||||
onAddFiles,
|
|
||||||
onComposerHeightChange,
|
onComposerHeightChange,
|
||||||
onMessageSent,
|
onMessageSent,
|
||||||
}: {
|
}: {
|
||||||
@@ -1376,8 +1293,6 @@ const AgentComposerSection = memo(function AgentComposerSection({
|
|||||||
agentInputDraft: AgentInputDraft;
|
agentInputDraft: AgentInputDraft;
|
||||||
onAttentionInputFocus: () => void;
|
onAttentionInputFocus: () => void;
|
||||||
onAttentionPromptSend: () => void;
|
onAttentionPromptSend: () => 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;
|
||||||
}) {
|
}) {
|
||||||
@@ -1401,8 +1316,6 @@ const AgentComposerSection = memo(function AgentComposerSection({
|
|||||||
agentInputDraft={agentInputDraft}
|
agentInputDraft={agentInputDraft}
|
||||||
onAttentionInputFocus={onAttentionInputFocus}
|
onAttentionInputFocus={onAttentionInputFocus}
|
||||||
onAttentionPromptSend={onAttentionPromptSend}
|
onAttentionPromptSend={onAttentionPromptSend}
|
||||||
onAddImages={onAddImages}
|
|
||||||
onAddFiles={onAddFiles}
|
|
||||||
onComposerHeightChange={onComposerHeightChange}
|
onComposerHeightChange={onComposerHeightChange}
|
||||||
onMessageSent={onMessageSent}
|
onMessageSent={onMessageSent}
|
||||||
/>
|
/>
|
||||||
@@ -1418,8 +1331,6 @@ function ActiveAgentComposer({
|
|||||||
agentInputDraft,
|
agentInputDraft,
|
||||||
onAttentionInputFocus,
|
onAttentionInputFocus,
|
||||||
onAttentionPromptSend,
|
onAttentionPromptSend,
|
||||||
onAddImages,
|
|
||||||
onAddFiles,
|
|
||||||
onComposerHeightChange,
|
onComposerHeightChange,
|
||||||
onMessageSent,
|
onMessageSent,
|
||||||
}: {
|
}: {
|
||||||
@@ -1431,8 +1342,6 @@ function ActiveAgentComposer({
|
|||||||
agentInputDraft: AgentInputDraft;
|
agentInputDraft: AgentInputDraft;
|
||||||
onAttentionInputFocus: () => void;
|
onAttentionInputFocus: () => void;
|
||||||
onAttentionPromptSend: () => void;
|
onAttentionPromptSend: () => 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;
|
||||||
}) {
|
}) {
|
||||||
@@ -1578,8 +1487,6 @@ function ActiveAgentComposer({
|
|||||||
isSubmitLoading={isSubmitLoading}
|
isSubmitLoading={isSubmitLoading}
|
||||||
onAttentionInputFocus={onAttentionInputFocus}
|
onAttentionInputFocus={onAttentionInputFocus}
|
||||||
onAttentionPromptSend={onAttentionPromptSend}
|
onAttentionPromptSend={onAttentionPromptSend}
|
||||||
onAddImages={onAddImages}
|
|
||||||
onAddFiles={onAddFiles}
|
|
||||||
onComposerHeightChange={onComposerHeightChange}
|
onComposerHeightChange={onComposerHeightChange}
|
||||||
onMessageSent={onMessageSent}
|
onMessageSent={onMessageSent}
|
||||||
onClientSlashCommand={handleClientSlashCommand}
|
onClientSlashCommand={handleClientSlashCommand}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Check, ChevronDown, Folder, GitBranch, GitPullRequest, X } from "lucide
|
|||||||
import { Composer } from "@/composer";
|
import { Composer } from "@/composer";
|
||||||
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||||
import { splitComposerAttachmentsForSubmit } from "@/composer/attachments/submit";
|
import { splitComposerAttachmentsForSubmit } from "@/composer/attachments/submit";
|
||||||
import { FileDropZone } from "@/components/file-drop-zone";
|
|
||||||
import { HostStatusDot } from "@/components/host-status-dot";
|
import { HostStatusDot } from "@/components/host-status-dot";
|
||||||
import { HostPicker } from "@/components/hosts/host-picker";
|
import { HostPicker } from "@/components/hosts/host-picker";
|
||||||
import { ProjectIconView } from "@/components/project-icon-view";
|
import { ProjectIconView } from "@/components/project-icon-view";
|
||||||
@@ -57,7 +56,7 @@ import {
|
|||||||
} from "@/projects/host-projects";
|
} from "@/projects/host-projects";
|
||||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||||
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
||||||
import type { ImageAttachment, MessagePayload } from "@/composer/types";
|
import type { MessagePayload } from "@/composer/types";
|
||||||
import type { AgentAttachment, GitHubSearchItem } from "@getpaseo/protocol/messages";
|
import type { AgentAttachment, GitHubSearchItem } from "@getpaseo/protocol/messages";
|
||||||
import type { CreatePaseoWorktreeInput } from "@getpaseo/client/internal/daemon-client";
|
import type { CreatePaseoWorktreeInput } from "@getpaseo/client/internal/daemon-client";
|
||||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||||
@@ -1809,14 +1808,6 @@ export function NewWorkspaceScreen({
|
|||||||
[composerState, draftKey, ensureWorkspace, selectedServerId, t, toast],
|
[composerState, draftKey, ensureWorkspace, selectedServerId, t, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
|
||||||
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
|
||||||
addImagesRef.current = addImages;
|
|
||||||
}, []);
|
|
||||||
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
|
||||||
addImagesRef.current?.(files);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderPickerOption = useCallback(
|
const renderPickerOption = useCallback(
|
||||||
(props: {
|
(props: {
|
||||||
option: ComboboxOptionType;
|
option: ComboboxOptionType;
|
||||||
@@ -1974,46 +1965,43 @@ export function NewWorkspaceScreen({
|
|||||||
const screenHeaderLeft = useMemo(() => <SidebarMenuToggle />, []);
|
const screenHeaderLeft = useMemo(() => <SidebarMenuToggle />, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileDropZone onFilesDropped={handleFilesDropped}>
|
<View style={styles.container}>
|
||||||
<View style={styles.container}>
|
<ScreenHeader left={screenHeaderLeft} borderless />
|
||||||
<ScreenHeader left={screenHeaderLeft} borderless />
|
<View style={contentStyle}>
|
||||||
<View style={contentStyle}>
|
<TitlebarDragRegion />
|
||||||
<TitlebarDragRegion />
|
<ReanimatedAnimated.View style={centeredStyle}>
|
||||||
<ReanimatedAnimated.View style={centeredStyle}>
|
<View style={styles.composerTitleContainer}>
|
||||||
<View style={styles.composerTitleContainer}>
|
<Text style={styles.composerTitle}>{t("newWorkspace.title")}</Text>
|
||||||
<Text style={styles.composerTitle}>{t("newWorkspace.title")}</Text>
|
</View>
|
||||||
</View>
|
{formStack}
|
||||||
{formStack}
|
<Composer
|
||||||
<Composer
|
externalKeyboardShift
|
||||||
externalKeyboardShift
|
agentId={draftKey}
|
||||||
agentId={draftKey}
|
serverId={selectedServerId}
|
||||||
serverId={selectedServerId}
|
isPaneFocused={true}
|
||||||
isPaneFocused={true}
|
onSubmitMessage={handleSubmitNewWorkspace}
|
||||||
onSubmitMessage={handleSubmitNewWorkspace}
|
allowEmptySubmit={true}
|
||||||
allowEmptySubmit={true}
|
submitButtonAccessibilityLabel={t("newWorkspace.create")}
|
||||||
submitButtonAccessibilityLabel={t("newWorkspace.create")}
|
submitButtonTestID="workspace-create-submit"
|
||||||
submitButtonTestID="workspace-create-submit"
|
submitIcon="return"
|
||||||
submitIcon="return"
|
isSubmitLoading={pendingAction !== null}
|
||||||
isSubmitLoading={pendingAction !== null}
|
submitBehavior="preserve-and-lock"
|
||||||
submitBehavior="preserve-and-lock"
|
blurOnSubmit={true}
|
||||||
blurOnSubmit={true}
|
value={chatDraft.text}
|
||||||
value={chatDraft.text}
|
onChangeText={chatDraft.setText}
|
||||||
onChangeText={chatDraft.setText}
|
attachments={chatDraft.attachments}
|
||||||
attachments={chatDraft.attachments}
|
onChangeAttachments={chatDraft.setAttachments}
|
||||||
onChangeAttachments={chatDraft.setAttachments}
|
cwd={selectedSourceDirectory ?? ""}
|
||||||
cwd={selectedSourceDirectory ?? ""}
|
clearDraft={handleClearDraft}
|
||||||
clearDraft={handleClearDraft}
|
autoFocus
|
||||||
autoFocus
|
commandDraftConfig={composerState?.commandDraftConfig}
|
||||||
commandDraftConfig={composerState?.commandDraftConfig}
|
agentControls={agentControlsWithDisabled}
|
||||||
agentControls={agentControlsWithDisabled}
|
footer={composerFooter}
|
||||||
onAddImages={handleAddImagesCallback}
|
/>
|
||||||
footer={composerFooter}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
/>
|
</ReanimatedAnimated.View>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
|
||||||
</ReanimatedAnimated.View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</FileDropZone>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user