mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
refactor(desktop): extract composer drag-and-drop into useComposerDrop
Lift the attachment drop engine (dragActive + the 7 drag/drop handlers + the in-app-ref vs OS-upload split) out of ChatBar into composer/hooks/use-composer-drop.ts. Self-contained, off the keystroke path — consumes insertInlineRefs + onAttachDroppedItems + requestMainFocus. Verbatim move, behaviour-preserving.
This commit is contained in:
parent
cf05b38683
commit
bd53230739
2 changed files with 174 additions and 129 deletions
164
apps/desktop/src/app/chat/composer/hooks/use-composer-drop.ts
Normal file
164
apps/desktop/src/app/chat/composer/hooks/use-composer-drop.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { type DragEvent as ReactDragEvent, useRef, useState } from 'react'
|
||||
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
|
||||
import { extractDroppedFiles, HERMES_PATHS_MIME, partitionDroppedFiles } from '../../hooks/use-composer-actions'
|
||||
import { dragHasAttachments, droppedFileInlineRefs, type InlineRefInput } from '../inline-refs'
|
||||
import type { ChatBarProps } from '../types'
|
||||
|
||||
interface UseComposerDropArgs {
|
||||
cwd: ChatBarProps['cwd']
|
||||
insertInlineRefs: (refs: InlineRefInput[]) => boolean
|
||||
onAttachDroppedItems: ChatBarProps['onAttachDroppedItems']
|
||||
requestMainFocus: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag-and-drop attachment engine. Splits drops by origin: in-app drags
|
||||
* (project tree / gutter) stay inline `@file:`/`@line:` refs the gateway
|
||||
* resolves directly; OS/Finder drops (absolute local paths a remote gateway
|
||||
* can't read, image bytes vision needs) route through the upload pipeline.
|
||||
* Off the keystroke path; consumes `insertInlineRefs` + the attach handler.
|
||||
*/
|
||||
export function useComposerDrop({
|
||||
cwd,
|
||||
insertInlineRefs,
|
||||
onAttachDroppedItems,
|
||||
requestMainFocus
|
||||
}: UseComposerDropArgs) {
|
||||
const [dragActive, setDragActive] = useState(false)
|
||||
const dragDepthRef = useRef(0)
|
||||
|
||||
const resetDragState = () => {
|
||||
dragDepthRef.current = 0
|
||||
setDragActive(false)
|
||||
}
|
||||
|
||||
const handleDragEnter = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems || !dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
dragDepthRef.current += 1
|
||||
|
||||
if (!dragActive) {
|
||||
setDragActive(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems || !dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)
|
||||
|
||||
if (dragDepthRef.current === 0) {
|
||||
setDragActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
resetDragState()
|
||||
|
||||
const candidates = extractDroppedFiles(event.dataTransfer)
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// In-app drags (project tree / gutter) are workspace-relative paths the
|
||||
// gateway resolves directly, so they stay inline @file:/@line: refs. OS
|
||||
// drops are absolute local paths a remote gateway can't read (and images
|
||||
// need byte upload for vision), so route them through the upload pipeline.
|
||||
const { inAppRefs, osDrops } = partitionDroppedFiles(candidates)
|
||||
const refs = droppedFileInlineRefs(inAppRefs, cwd)
|
||||
|
||||
if (refs.length && insertInlineRefs(refs)) {
|
||||
triggerHaptic('selection')
|
||||
}
|
||||
|
||||
if (osDrops.length) {
|
||||
void Promise.resolve(onAttachDroppedItems(osDrops)).then(attached => {
|
||||
if (attached) {
|
||||
triggerHaptic('selection')
|
||||
requestMainFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputDragOver = (event: ReactDragEvent<HTMLDivElement>) => {
|
||||
if (!dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
|
||||
const handleInputDrop = (event: ReactDragEvent<HTMLDivElement>) => {
|
||||
if (!dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
const candidates = extractDroppedFiles(event.dataTransfer)
|
||||
|
||||
if (!candidates.length) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resetDragState()
|
||||
|
||||
// Dropping straight onto the text box used to inline-ref *every* file —
|
||||
// including OS/Finder drops, whose absolute local path a remote gateway
|
||||
// can't read and whose image bytes never reached vision. Split by origin:
|
||||
// in-app drags stay inline refs; OS drops go through the upload pipeline.
|
||||
// (When no upload handler is wired, fall back to inline refs for all.)
|
||||
const attach = onAttachDroppedItems
|
||||
const { inAppRefs, osDrops } = partitionDroppedFiles(candidates)
|
||||
const refs = droppedFileInlineRefs(attach ? inAppRefs : candidates, cwd)
|
||||
|
||||
if (refs.length && insertInlineRefs(refs)) {
|
||||
triggerHaptic('selection')
|
||||
}
|
||||
|
||||
if (attach && osDrops.length) {
|
||||
void Promise.resolve(attach(osDrops)).then(attached => {
|
||||
if (attached) {
|
||||
triggerHaptic('selection')
|
||||
requestMainFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dragActive,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
handleDragOver,
|
||||
handleDrop,
|
||||
handleInputDragOver,
|
||||
handleInputDrop
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import {
|
|||
type ClipboardEvent,
|
||||
type FormEvent,
|
||||
type KeyboardEvent,
|
||||
type DragEvent as ReactDragEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
|
|
@ -69,8 +68,6 @@ import { $autoSpeakReplies } from '@/store/voice-prefs'
|
|||
import { isSecondaryWindow } from '@/store/windows'
|
||||
import { useTheme } from '@/themes'
|
||||
|
||||
import { extractDroppedFiles, HERMES_PATHS_MIME, partitionDroppedFiles } from '../hooks/use-composer-actions'
|
||||
|
||||
import { AttachmentList } from './attachments'
|
||||
import {
|
||||
cloneAttachments,
|
||||
|
|
@ -97,13 +94,12 @@ import {
|
|||
} from './focus'
|
||||
import { HelpHint } from './help-hint'
|
||||
import { useAtCompletions } from './hooks/use-at-completions'
|
||||
import { useComposerDrop } from './hooks/use-composer-drop'
|
||||
import { useComposerMetrics } from './hooks/use-composer-metrics'
|
||||
import { useComposerVoice } from './hooks/use-composer-voice'
|
||||
import { useComposerPopoutGestures } from './hooks/use-popout-drag'
|
||||
import { useSlashCompletions } from './hooks/use-slash-completions'
|
||||
import {
|
||||
dragHasAttachments,
|
||||
droppedFileInlineRefs,
|
||||
type InlineRefInput,
|
||||
insertInlineRefsIntoEditor
|
||||
} from './inline-refs'
|
||||
|
|
@ -281,12 +277,10 @@ export function ChatBar({
|
|||
|
||||
const [urlOpen, setUrlOpen] = useState(false)
|
||||
const [urlValue, setUrlValue] = useState('')
|
||||
const [dragActive, setDragActive] = useState(false)
|
||||
const [queueEdit, setQueueEdit] = useState<QueueEditState | null>(null)
|
||||
const [focusRequestId, setFocusRequestId] = useState(0)
|
||||
const queueEditRef = useRef(queueEdit)
|
||||
queueEditRef.current = queueEdit
|
||||
const dragDepthRef = useRef(0)
|
||||
const composingRef = useRef(false) // true during IME composition (CJK input)
|
||||
|
||||
const { availableThemes, themeName } = useTheme()
|
||||
|
|
@ -1114,128 +1108,15 @@ export function ChatBar({
|
|||
window.setTimeout(refreshTrigger, 0)
|
||||
}
|
||||
|
||||
const resetDragState = () => {
|
||||
dragDepthRef.current = 0
|
||||
setDragActive(false)
|
||||
}
|
||||
|
||||
const handleDragEnter = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems || !dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
dragDepthRef.current += 1
|
||||
|
||||
if (!dragActive) {
|
||||
setDragActive(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems || !dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)
|
||||
|
||||
if (dragDepthRef.current === 0) {
|
||||
setDragActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (event: ReactDragEvent<HTMLFormElement>) => {
|
||||
if (!onAttachDroppedItems) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
resetDragState()
|
||||
|
||||
const candidates = extractDroppedFiles(event.dataTransfer)
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// In-app drags (project tree / gutter) are workspace-relative paths the
|
||||
// gateway resolves directly, so they stay inline @file:/@line: refs. OS
|
||||
// drops are absolute local paths a remote gateway can't read (and images
|
||||
// need byte upload for vision), so route them through the upload pipeline.
|
||||
const { inAppRefs, osDrops } = partitionDroppedFiles(candidates)
|
||||
const refs = droppedFileInlineRefs(inAppRefs, cwd)
|
||||
|
||||
if (refs.length && insertInlineRefs(refs)) {
|
||||
triggerHaptic('selection')
|
||||
}
|
||||
|
||||
if (osDrops.length) {
|
||||
void Promise.resolve(onAttachDroppedItems(osDrops)).then(attached => {
|
||||
if (attached) {
|
||||
triggerHaptic('selection')
|
||||
requestMainFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputDragOver = (event: ReactDragEvent<HTMLDivElement>) => {
|
||||
if (!dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
|
||||
const handleInputDrop = (event: ReactDragEvent<HTMLDivElement>) => {
|
||||
if (!dragHasAttachments(event.dataTransfer, HERMES_PATHS_MIME)) {
|
||||
return
|
||||
}
|
||||
|
||||
const candidates = extractDroppedFiles(event.dataTransfer)
|
||||
|
||||
if (!candidates.length) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resetDragState()
|
||||
|
||||
// Dropping straight onto the text box used to inline-ref *every* file —
|
||||
// including OS/Finder drops, whose absolute local path a remote gateway
|
||||
// can't read and whose image bytes never reached vision. Split by origin:
|
||||
// in-app drags stay inline refs; OS drops go through the upload pipeline.
|
||||
// (When no upload handler is wired, fall back to inline refs for all.)
|
||||
const attach = onAttachDroppedItems
|
||||
const { inAppRefs, osDrops } = partitionDroppedFiles(candidates)
|
||||
const refs = droppedFileInlineRefs(attach ? inAppRefs : candidates, cwd)
|
||||
|
||||
if (refs.length && insertInlineRefs(refs)) {
|
||||
triggerHaptic('selection')
|
||||
}
|
||||
|
||||
if (attach && osDrops.length) {
|
||||
void Promise.resolve(attach(osDrops)).then(attached => {
|
||||
if (attached) {
|
||||
triggerHaptic('selection')
|
||||
requestMainFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const {
|
||||
dragActive,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
handleDragOver,
|
||||
handleDrop,
|
||||
handleInputDragOver,
|
||||
handleInputDrop
|
||||
} = useComposerDrop({ cwd, insertInlineRefs, onAttachDroppedItems, requestMainFocus })
|
||||
|
||||
const clearDraft = useCallback(() => {
|
||||
setComposerText('')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue