diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 1e1bb3bd8..0f1d5c5a3 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webUtils } from "electron"; import type { DesktopBridge } from "@t3tools/contracts"; const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; @@ -15,6 +15,7 @@ const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null; contextBridge.exposeInMainWorld("desktopBridge", { getWsUrl: () => wsUrl, + getPathForFile: (file: File) => webUtils.getPathForFile(file), pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL), confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message), setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme), diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 3c8a0a152..8d60ce798 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -262,6 +262,7 @@ const EMPTY_AVAILABLE_EDITORS: EditorId[] = []; const EMPTY_PROVIDER_STATUSES: ServerProviderStatus[] = []; const EMPTY_PENDING_USER_INPUT_ANSWERS: Record = {}; const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; +const COMPOSER_FILE_PATH_SEPARATOR = " "; const SCRIPT_TERMINAL_COLS = 120; const SCRIPT_TERMINAL_ROWS = 30; const WORKTREE_BRANCH_PREFIX = "t3code"; @@ -2450,17 +2451,32 @@ export default function ChatView({ threadId }: ChatViewProps) { removeComposerImageFromDraft(imageId); }; + const addComposerAttachments = (files: File[]) => { + const imageFiles = files.filter((f) => f.type.startsWith("image/")); + const nonImageFiles = files.filter((f) => !f.type.startsWith("image/")); + + if (imageFiles.length > 0) { + addComposerImages(imageFiles); + } + + if (nonImageFiles.length > 0) { + const paths = nonImageFiles.map( + (file) => window.desktopBridge?.getPathForFile(file) ?? file.name, + ); + const insertion = paths + .map((p) => (p.includes(" ") ? `"${p}"` : p)) + .join(COMPOSER_FILE_PATH_SEPARATOR); + composerEditorRef.current?.insertTextAndFocus(insertion); + } + }; + const onComposerPaste = (event: React.ClipboardEvent) => { const files = Array.from(event.clipboardData.files); if (files.length === 0) { return; } - const imageFiles = files.filter((file) => file.type.startsWith("image/")); - if (imageFiles.length === 0) { - return; - } event.preventDefault(); - addComposerImages(imageFiles); + addComposerAttachments(files); }; const onComposerDragEnter = (event: React.DragEvent) => { @@ -2504,8 +2520,9 @@ export default function ChatView({ threadId }: ChatViewProps) { dragDepthRef.current = 0; setIsDragOverComposer(false); const files = Array.from(event.dataTransfer.files); - addComposerImages(files); - focusComposer(); + const hasNonImageFiles = files.some((f) => !f.type.startsWith("image/")); + addComposerAttachments(files); + if (!hasNonImageFiles) focusComposer(); }; const onRevertToTurnCount = useCallback( diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 96efc0fbf..dc5f3e28b 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -384,6 +384,7 @@ export interface ComposerPromptEditorHandle { focusAt: (cursor: number) => void; focusAtEnd: () => void; readSnapshot: () => { value: string; cursor: number }; + insertTextAndFocus: (text: string) => void; } interface ComposerPromptEditorProps { @@ -699,6 +700,22 @@ function ComposerPromptEditorInner({ return snapshot; }, [editor]); + const insertTextAndFocus = useCallback( + (text: string) => { + const rootElement = editor.getRootElement(); + if (rootElement) { + rootElement.focus(); + } + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.insertText(text); + } + }); + }, + [editor], + ); + useImperativeHandle( editorRef, () => ({ @@ -712,8 +729,9 @@ function ComposerPromptEditorInner({ focusAt(snapshotRef.current.value.length); }, readSnapshot, + insertTextAndFocus, }), - [focusAt, readSnapshot], + [focusAt, readSnapshot, insertTextAndFocus], ); const handleEditorChange = useCallback((editorState: EditorState) => { diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index b9127fb17..959d70fce 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -96,6 +96,7 @@ export interface DesktopUpdateActionResult { export interface DesktopBridge { getWsUrl: () => string | null; + getPathForFile: (file: File) => string; pickFolder: () => Promise; confirm: (message: string) => Promise; setTheme: (theme: DesktopTheme) => Promise;