feat: add workspace files to chat from Files and Changes
* feat(app): open changed files from context menu * fix(app): preserve changed-file row behavior * feat(app): add changed files to open chats * feat(app): share file actions across explorer and changes * feat(app): attach workspace files to focused chat * feat(app): drag workspace files into chat * refactor(app): use file actions menu for changed files * fix(app): open changed-file actions on right click * test(app): cover direct file attachment flow * fix(app): enable add to chat on native * fix(app): align workspace file pane rows * refactor(app): simplify workspace file attachments * fix(app): preserve uploaded file draft attachments --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
42ee5a5949
commit
bd937850b3
@@ -338,7 +338,7 @@ export function openComposerAttachment(input: OpenComposerAttachmentInput): void
|
||||
input.setLightboxMetadata(input.attachment.metadata);
|
||||
return;
|
||||
}
|
||||
if (input.attachment.kind === "file") {
|
||||
if (input.attachment.kind === "file" || input.attachment.kind === "workspace_file") {
|
||||
return;
|
||||
}
|
||||
if (isWorkspaceAttachment(input.attachment)) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
buildForgeAttachmentFromSearchItem,
|
||||
buildLegacyGitHubAttachmentFromSearchItem,
|
||||
} from "@/utils/review-attachments";
|
||||
import { workspaceFileAttachmentToAgentAttachment } from "@/attachments/workspace-file";
|
||||
|
||||
export type ComposerAttachmentSubmitFormat = "forge" | "legacy-github";
|
||||
|
||||
@@ -49,6 +50,11 @@ export function splitComposerAttachmentsForSubmit(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attachment.kind === "workspace_file") {
|
||||
agentAttachments.push(workspaceFileAttachmentToAgentAttachment(attachment));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isWorkspaceAttachment(attachment)) {
|
||||
if (attachment.kind === "browser_element" && attachment.attachment.screenshot) {
|
||||
images.push(attachment.attachment.screenshot);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useDraftStore } from "@/stores/draft-store";
|
||||
import type { AttachmentMetadata, ComposerAttachment } from "@/attachments/types";
|
||||
import { createWorkspaceFileAttachment } from "@/attachments/workspace-file";
|
||||
|
||||
const { asyncStorage } = vi.hoisted(() => ({
|
||||
asyncStorage: new Map<string, string>(),
|
||||
@@ -159,7 +160,11 @@ describe("useAgentInputDraft live contract", () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
useDraftStore.setState({ drafts: {}, createModalDraft: null });
|
||||
useDraftStore.setState({
|
||||
drafts: {},
|
||||
createModalDraft: null,
|
||||
attachmentFocusRequestByDraftKey: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates persisted text and attachments and returns draft-mode composer state for a caller-provided key", async () => {
|
||||
@@ -424,6 +429,39 @@ describe("useAgentInputDraft live contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches to an unmounted legacy draft without losing its input", async () => {
|
||||
const image: AttachmentMetadata = {
|
||||
id: "legacy-image",
|
||||
mimeType: "image/png",
|
||||
storageType: "web-indexeddb",
|
||||
storageKey: "attachments/legacy-image",
|
||||
createdAt: 10,
|
||||
};
|
||||
useDraftStore.setState({
|
||||
drafts: {
|
||||
"draft:legacy-workspace-file": {
|
||||
input: { text: "legacy text", images: [image] },
|
||||
lifecycle: "active",
|
||||
updatedAt: Date.now(),
|
||||
version: 1,
|
||||
} as unknown as DraftRecordForTest,
|
||||
},
|
||||
});
|
||||
|
||||
await useDraftStore.getState().attachWorkspaceFile({
|
||||
draftKey: "draft:legacy-workspace-file",
|
||||
attachment: createWorkspaceFileAttachment({ path: "src/app.ts" }),
|
||||
});
|
||||
|
||||
expect(useDraftStore.getState().getDraftInput("draft:legacy-workspace-file")).toEqual({
|
||||
text: "legacy text",
|
||||
attachments: [
|
||||
{ kind: "image", metadata: image },
|
||||
createWorkspaceFileAttachment({ path: "src/app.ts" }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("clear resets text and attachments", async () => {
|
||||
let latest: ReturnType<typeof useAgentInputDraft> | null = null;
|
||||
const image: AttachmentMetadata = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { UserComposerAttachment } from "@/attachments/types";
|
||||
import type { DraftAgentControlsProps } from "@/composer/agent-controls";
|
||||
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "@/hooks/use-agent-form-state";
|
||||
import { useDraftAgentFeatures } from "@/hooks/use-draft-agent-features";
|
||||
import {
|
||||
areAttachmentsEqual,
|
||||
buildDraftAgentControls,
|
||||
hasDraftContent,
|
||||
resolveDraftKey,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
type ProviderSelectionState,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
import { useDraftStore } from "@/stores/draft-store";
|
||||
import { toDraftInputIfReady } from "@/stores/draft-store/state";
|
||||
|
||||
type AttachmentUpdater =
|
||||
| UserComposerAttachment[]
|
||||
@@ -57,6 +57,7 @@ export interface AgentInputDraft {
|
||||
setAttachments: (updater: AttachmentUpdater) => void;
|
||||
clear: (lifecycle: "sent" | "abandoned") => void;
|
||||
isHydrated: boolean;
|
||||
attachmentFocusRequestId: number;
|
||||
composerState: DraftComposerState | null;
|
||||
}
|
||||
|
||||
@@ -77,67 +78,66 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
|
||||
}),
|
||||
[formState.selectedServerId, input.draftKey],
|
||||
);
|
||||
const [text, setText] = useState("");
|
||||
const [attachments, setAttachmentsState] = useState<UserComposerAttachment[]>([]);
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
const draftGenerationRef = useRef(0);
|
||||
const hydratedGenerationRef = useRef(0);
|
||||
const draftRecord = useDraftStore((state) => state.drafts[draftKey]);
|
||||
const draft = useMemo(() => toDraftInputIfReady(draftRecord), [draftRecord]);
|
||||
const attachmentFocusRequestId = useDraftStore(
|
||||
(state) => state.attachmentFocusRequestByDraftKey[draftKey] ?? 0,
|
||||
);
|
||||
const [hydratedDraftKey, setHydratedDraftKey] = useState<string | null>(null);
|
||||
const text = draft?.text ?? "";
|
||||
const attachments = draft?.attachments ?? [];
|
||||
const isHydrated = hydratedDraftKey === draftKey;
|
||||
|
||||
const setAttachments = useCallback((updater: AttachmentUpdater) => {
|
||||
setAttachmentsState((previousAttachments) => {
|
||||
if (typeof updater === "function") {
|
||||
return updater(previousAttachments);
|
||||
const saveDraft = useCallback(
|
||||
(
|
||||
update: (draft: { text: string; attachments: UserComposerAttachment[] }) => {
|
||||
text: string;
|
||||
attachments: UserComposerAttachment[];
|
||||
},
|
||||
) => {
|
||||
const store = useDraftStore.getState();
|
||||
const current = store.getDraftInput(draftKey) ?? { text: "", attachments: [] };
|
||||
const next = update(current);
|
||||
if (!hasDraftContent(next)) {
|
||||
store.clearDraftInput({ draftKey, lifecycle: "abandoned" });
|
||||
return;
|
||||
}
|
||||
return updater;
|
||||
});
|
||||
}, []);
|
||||
store.saveDraftInput({ draftKey, draft: next });
|
||||
},
|
||||
[draftKey],
|
||||
);
|
||||
|
||||
const setText = useCallback(
|
||||
(nextText: string) => {
|
||||
saveDraft((current) => ({ ...current, text: nextText }));
|
||||
},
|
||||
[saveDraft],
|
||||
);
|
||||
|
||||
const setAttachments = useCallback(
|
||||
(updater: AttachmentUpdater) => {
|
||||
saveDraft((current) => ({
|
||||
...current,
|
||||
attachments: typeof updater === "function" ? updater(current.attachments) : updater,
|
||||
}));
|
||||
},
|
||||
[saveDraft],
|
||||
);
|
||||
|
||||
const clear = useCallback(
|
||||
(lifecycle: "sent" | "abandoned") => {
|
||||
const store = useDraftStore.getState();
|
||||
store.clearDraftInput({ draftKey, lifecycle });
|
||||
|
||||
const generation = store.beginDraftGeneration(draftKey);
|
||||
draftGenerationRef.current = generation;
|
||||
hydratedGenerationRef.current = generation;
|
||||
|
||||
setText("");
|
||||
setAttachmentsState([]);
|
||||
setIsHydrated(true);
|
||||
useDraftStore.getState().clearDraftInput({ draftKey, lifecycle });
|
||||
},
|
||||
[draftKey],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const store = useDraftStore.getState();
|
||||
const generation = store.beginDraftGeneration(draftKey);
|
||||
draftGenerationRef.current = generation;
|
||||
hydratedGenerationRef.current = 0;
|
||||
|
||||
setText("");
|
||||
setAttachmentsState([]);
|
||||
setIsHydrated(false);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
const draft = await store.hydrateDraftInput({
|
||||
draftKey,
|
||||
});
|
||||
if (cancelled) {
|
||||
return;
|
||||
await useDraftStore.getState().hydrateDraftInput({ draftKey });
|
||||
if (!cancelled) {
|
||||
setHydratedDraftKey(draftKey);
|
||||
}
|
||||
if (!useDraftStore.getState().isDraftGenerationCurrent({ draftKey, generation })) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (draft) {
|
||||
setText(draft.text);
|
||||
setAttachmentsState(draft.attachments);
|
||||
}
|
||||
|
||||
hydratedGenerationRef.current = generation;
|
||||
setIsHydrated(true);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
@@ -145,52 +145,6 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
|
||||
};
|
||||
}, [draftKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentGeneration = draftGenerationRef.current;
|
||||
if (currentGeneration <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = useDraftStore.getState();
|
||||
const isCurrentGeneration = store.isDraftGenerationCurrent({
|
||||
draftKey,
|
||||
generation: currentGeneration,
|
||||
});
|
||||
if (!isCurrentGeneration) {
|
||||
return;
|
||||
}
|
||||
if (hydratedGenerationRef.current !== currentGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = store.getDraftInput(draftKey);
|
||||
const isSameDraft =
|
||||
existing !== undefined &&
|
||||
existing.text === text &&
|
||||
areAttachmentsEqual({
|
||||
left: existing.attachments,
|
||||
right: attachments,
|
||||
});
|
||||
if (isSameDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasDraftContent({ text, attachments })) {
|
||||
if (existing) {
|
||||
store.clearDraftInput({ draftKey, lifecycle: "abandoned" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
store.saveDraftInput({
|
||||
draftKey,
|
||||
draft: {
|
||||
text,
|
||||
attachments,
|
||||
},
|
||||
});
|
||||
}, [attachments, draftKey, text]);
|
||||
|
||||
const lockedWorkingDir = composerOptions?.lockedWorkingDir?.trim() ?? "";
|
||||
useEffect(() => {
|
||||
if (!composerOptions || !lockedWorkingDir) {
|
||||
@@ -304,6 +258,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
|
||||
setAttachments,
|
||||
clear,
|
||||
isHydrated,
|
||||
attachmentFocusRequestId,
|
||||
composerState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -751,6 +751,7 @@ export function WorkspaceDraftAgentTab({
|
||||
<Composer
|
||||
agentId={tabId}
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
externalKeyboardShift
|
||||
isPaneFocused={isPaneFocused}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
@@ -765,6 +766,7 @@ export function WorkspaceDraftAgentTab({
|
||||
cwd={composerState.workingDir}
|
||||
clearDraft={draftInput.clear}
|
||||
autoFocus={shouldAutoFocusWorkspaceDraftComposer({ isPaneFocused, isSubmitting })}
|
||||
autoFocusKey={String(draftInput.attachmentFocusRequestId)}
|
||||
onFocusInput={handleFocusInputCallback}
|
||||
commandDraftConfig={composerState.commandDraftConfig}
|
||||
agentControls={composerAgentControls}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkspaceLayout } from "@/stores/workspace-layout-store";
|
||||
import { resolveFocusedChatTarget } from "./focused-chat-target";
|
||||
|
||||
function layoutWithTarget(
|
||||
target: import("@/stores/workspace-tabs-store").WorkspaceTab["target"],
|
||||
): WorkspaceLayout {
|
||||
return {
|
||||
root: {
|
||||
kind: "pane",
|
||||
pane: {
|
||||
id: "pane-1",
|
||||
tabIds: ["focused-tab"],
|
||||
focusedTabId: "focused-tab",
|
||||
tabs: [{ tabId: "focused-tab", target, createdAt: 1 }],
|
||||
},
|
||||
},
|
||||
focusedPaneId: "pane-1",
|
||||
} as WorkspaceLayout;
|
||||
}
|
||||
|
||||
describe("focused chat target", () => {
|
||||
it("targets the focused agent draft", () => {
|
||||
expect(
|
||||
resolveFocusedChatTarget({
|
||||
serverId: "server-1",
|
||||
layout: layoutWithTarget({ kind: "agent", agentId: "agent-1" }),
|
||||
}),
|
||||
).toEqual({ tabId: "focused-tab", draftKey: "agent:server-1:agent-1" });
|
||||
});
|
||||
|
||||
it("targets the focused unsent draft", () => {
|
||||
expect(
|
||||
resolveFocusedChatTarget({
|
||||
serverId: "server-1",
|
||||
layout: layoutWithTarget({ kind: "draft", draftId: "draft-1" }),
|
||||
}),
|
||||
).toEqual({ tabId: "focused-tab", draftKey: "draft:server-1:draft-1" });
|
||||
});
|
||||
|
||||
it("does not guess when the focused tab is not a chat", () => {
|
||||
expect(
|
||||
resolveFocusedChatTarget({
|
||||
serverId: "server-1",
|
||||
layout: layoutWithTarget({ kind: "terminal", terminalId: "terminal-1" }),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { buildDraftStoreKey } from "@/stores/draft-keys";
|
||||
import {
|
||||
collectAllTabs,
|
||||
findPaneById,
|
||||
type WorkspaceLayout,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
|
||||
export interface FocusedChatTarget {
|
||||
tabId: string;
|
||||
draftKey: string;
|
||||
}
|
||||
|
||||
export function resolveFocusedChatTarget(input: {
|
||||
serverId: string;
|
||||
layout: WorkspaceLayout | undefined;
|
||||
}): FocusedChatTarget | null {
|
||||
if (!input.layout) {
|
||||
return null;
|
||||
}
|
||||
const pane = findPaneById(input.layout.root, input.layout.focusedPaneId);
|
||||
const focusedTabId = pane?.focusedTabId;
|
||||
if (!focusedTabId) {
|
||||
return null;
|
||||
}
|
||||
const tab = collectAllTabs(input.layout.root).find(
|
||||
(candidate) => candidate.tabId === focusedTabId,
|
||||
);
|
||||
if (!tab) {
|
||||
return null;
|
||||
}
|
||||
if (tab.target.kind === "agent") {
|
||||
return {
|
||||
tabId: tab.tabId,
|
||||
draftKey: buildDraftStoreKey({ serverId: input.serverId, agentId: tab.target.agentId }),
|
||||
};
|
||||
}
|
||||
if (tab.target.kind === "draft") {
|
||||
return {
|
||||
tabId: tab.tabId,
|
||||
draftKey: buildDraftStoreKey({
|
||||
serverId: input.serverId,
|
||||
agentId: tab.tabId,
|
||||
draftId: tab.target.draftId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -99,6 +99,7 @@ import type {
|
||||
AttachmentMetadata,
|
||||
ComposerAttachment,
|
||||
UserComposerAttachment,
|
||||
WorkspaceFileComposerAttachment,
|
||||
WorkspaceComposerAttachment,
|
||||
} from "@/attachments/types";
|
||||
import type { PickedFile } from "@/attachments/picked-file";
|
||||
@@ -119,6 +120,15 @@ import { getForgePresentation } from "@/git/forge";
|
||||
import { ForgeBrandIcon } from "@/git/forge-icon";
|
||||
import { useComposerGithubAutoAttach } from "./github/auto-attach";
|
||||
import { resolveClientSlashCommand, type ClientSlashCommand } from "@/client-slash-commands";
|
||||
import {
|
||||
appendWorkspaceFileAttachment,
|
||||
getWorkspaceFileAttachmentKey,
|
||||
getWorkspaceFileAttachmentSubtitle,
|
||||
} from "@/attachments/workspace-file";
|
||||
import {
|
||||
resolveWorkspaceFileDrop,
|
||||
type WorkspaceFileDragPayload,
|
||||
} from "@/attachments/workspace-file-drag";
|
||||
|
||||
type QueuedMessage = QueuedComposerMessage;
|
||||
|
||||
@@ -399,6 +409,18 @@ function renderComposerAttachmentPill(args: RenderComposerAttachmentPillArgs): R
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (attachment.kind === "workspace_file") {
|
||||
return (
|
||||
<WorkspaceFileAttachmentPill
|
||||
key={`workspace-file:${getWorkspaceFileAttachmentKey(attachment)}`}
|
||||
attachment={attachment}
|
||||
index={index}
|
||||
disabled={disabled}
|
||||
onRemove={onRemove}
|
||||
removeLabel={labels.removeFile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (composerWorkspaceAttachment.is(attachment)) {
|
||||
return composerWorkspaceAttachment.renderPill({
|
||||
attachment,
|
||||
@@ -711,6 +733,43 @@ function FileAttachmentPill({
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceFileAttachmentPillProps {
|
||||
attachment: WorkspaceFileComposerAttachment;
|
||||
index: number;
|
||||
disabled: boolean;
|
||||
onRemove: (index: number) => void;
|
||||
removeLabel: string;
|
||||
}
|
||||
|
||||
function WorkspaceFileAttachmentPill({
|
||||
attachment,
|
||||
index,
|
||||
disabled,
|
||||
onRemove,
|
||||
removeLabel,
|
||||
}: WorkspaceFileAttachmentPillProps) {
|
||||
const handleRemove = useCallback(() => {
|
||||
onRemove(index);
|
||||
}, [index, onRemove]);
|
||||
const fileName = attachment.path.split("/").pop() ?? attachment.path;
|
||||
return (
|
||||
<AttachmentPill
|
||||
testID="composer-workspace-file-attachment-pill"
|
||||
onOpen={noopCallback}
|
||||
onRemove={handleRemove}
|
||||
openAccessibilityLabel={fileName}
|
||||
removeAccessibilityLabel={removeLabel}
|
||||
disabled={disabled}
|
||||
>
|
||||
<AttachmentLabel
|
||||
icon={filePillIcon}
|
||||
title={fileName}
|
||||
subtitle={getWorkspaceFileAttachmentSubtitle(attachment)}
|
||||
/>
|
||||
</AttachmentPill>
|
||||
);
|
||||
}
|
||||
|
||||
interface GithubPickerOptionProps {
|
||||
label: string;
|
||||
testID: string;
|
||||
@@ -755,6 +814,7 @@ function GithubPickerOption({
|
||||
interface ComposerProps {
|
||||
agentId: string;
|
||||
serverId: string;
|
||||
workspaceId?: string | null;
|
||||
isPaneFocused: boolean;
|
||||
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
|
||||
onClientSlashCommand?: (command: ClientSlashCommand) => Promise<void>;
|
||||
@@ -786,6 +846,8 @@ interface ComposerProps {
|
||||
clearDraft: (lifecycle: "sent" | "abandoned") => void;
|
||||
/** When true, auto-focuses the text input on web. */
|
||||
autoFocus?: boolean;
|
||||
/** Changing this value requests focus again while autoFocus remains true. */
|
||||
autoFocusKey?: string;
|
||||
/** Callback to expose a focus function to parent components (desktop only). */
|
||||
onFocusInput?: (focus: () => void) => void;
|
||||
/** Optional draft context for listing commands before an agent exists. */
|
||||
@@ -977,6 +1039,7 @@ function ComposerVoiceModeButton({
|
||||
export function Composer({
|
||||
agentId,
|
||||
serverId,
|
||||
workspaceId,
|
||||
isPaneFocused,
|
||||
onSubmitMessage,
|
||||
onClientSlashCommand,
|
||||
@@ -1000,6 +1063,7 @@ export function Composer({
|
||||
cwd,
|
||||
clearDraft,
|
||||
autoFocus = false,
|
||||
autoFocusKey,
|
||||
onFocusInput,
|
||||
commandDraftConfig,
|
||||
onMessageSent,
|
||||
@@ -1195,6 +1259,21 @@ export function Composer({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleWorkspaceFileDropped = useCallback(
|
||||
(payload: WorkspaceFileDragPayload) => {
|
||||
if (!workspaceId) {
|
||||
return;
|
||||
}
|
||||
const attachment = resolveWorkspaceFileDrop({ payload, serverId, workspaceId });
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
setSelectedAttachments((current) => appendWorkspaceFileAttachment(current, attachment));
|
||||
focusInput();
|
||||
},
|
||||
[focusInput, serverId, setSelectedAttachments, workspaceId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onFocusInput?.(focusInput);
|
||||
}, [focusInput, onFocusInput]);
|
||||
@@ -1969,7 +2048,11 @@ export function Composer({
|
||||
// so a drop in that window would be lost or land on a locked draft. `disabled` hides the
|
||||
// backdrop and rejects the drop atomically, instead of accepting a drop with no feedback.
|
||||
useFileDrop(
|
||||
{ onFiles: addImages, onGenericFiles: handleGenericFilesDropped },
|
||||
{
|
||||
onFiles: addImages,
|
||||
onGenericFiles: handleGenericFilesDropped,
|
||||
onWorkspaceFile: handleWorkspaceFileDropped,
|
||||
},
|
||||
{ disabled: isSubmitBusy },
|
||||
);
|
||||
|
||||
@@ -2030,7 +2113,7 @@ export function Composer({
|
||||
isReadyForDictation={isDictationReady}
|
||||
placeholder={messagePlaceholder}
|
||||
autoFocus={messageInputAutoFocus}
|
||||
autoFocusKey={`${serverId}:${agentId}`}
|
||||
autoFocusKey={`${serverId}:${agentId}:${autoFocusKey ?? ""}`}
|
||||
disabled={isSubmitLoading}
|
||||
isPaneFocused={isPaneFocused}
|
||||
leftContent={leftContent}
|
||||
|
||||
Reference in New Issue
Block a user