refactor(desktop): extract composer "Add URL" dialog into useComposerUrlDialog

Moves the URL dialog's open/value state, autofocus-on-open effect, and submit
(host onAddUrl or an @url: directive) out of ChatBar into
hooks/use-composer-url-dialog.ts, verbatim. ChatBar just wires the returned
openUrlDialog into the context menu and the state into <UrlDialog>.
This commit is contained in:
Brooklyn Nicholson 2026-06-30 14:24:32 -05:00
parent e009ba57ac
commit 2a84bcb149
2 changed files with 59 additions and 32 deletions

View file

@ -0,0 +1,50 @@
import { useEffect, useRef, useState } from 'react'
import { triggerHaptic } from '@/lib/haptics'
interface UseComposerUrlDialogOptions {
insertText: (text: string) => void
onAddUrl?: (url: string) => void
}
/**
* "Add URL" dialog engine: open/value state, autofocus-on-open, and submit. On
* submit it prefers the host's `onAddUrl` (which may fetch/title the link) and
* otherwise drops an `@url:` directive into the draft.
*/
export function useComposerUrlDialog({ insertText, onAddUrl }: UseComposerUrlDialogOptions) {
const urlInputRef = useRef<HTMLInputElement | null>(null)
const [urlOpen, setUrlOpen] = useState(false)
const [urlValue, setUrlValue] = useState('')
useEffect(() => {
if (urlOpen) {
window.requestAnimationFrame(() => urlInputRef.current?.focus({ preventScroll: true }))
}
}, [urlOpen])
const openUrlDialog = () => {
triggerHaptic('open')
setUrlOpen(true)
}
const submitUrl = () => {
const url = urlValue.trim()
if (!url) {
return
}
if (onAddUrl) {
onAddUrl(url)
} else {
insertText(`@url:${url}`)
}
triggerHaptic('success')
setUrlValue('')
setUrlOpen(false)
}
return { openUrlDialog, setUrlOpen, setUrlValue, submitUrl, urlInputRef, urlOpen, urlValue }
}

View file

@ -68,6 +68,7 @@ import { useComposerEscCancel } from './hooks/use-composer-esc-cancel'
import { useComposerMetrics } from './hooks/use-composer-metrics'
import { useComposerQueue } from './hooks/use-composer-queue'
import { useComposerSubmit } from './hooks/use-composer-submit'
import { useComposerUrlDialog } from './hooks/use-composer-url-dialog'
import { useComposerVoice } from './hooks/use-composer-voice'
import { useComposerPopoutGestures } from './hooks/use-popout-drag'
import { useSlashCompletions } from './hooks/use-slash-completions'
@ -173,10 +174,6 @@ export function ChatBar({
position: popoutPosition
})
const urlInputRef = useRef<HTMLInputElement | null>(null)
const [urlOpen, setUrlOpen] = useState(false)
const [urlValue, setUrlValue] = useState('')
// Coordinator-owned: the draft engine reads the live queue-edit snapshot off
// this ref (to suppress its stash while editing a queued prompt) and the queue
// engine writes it — an explicit shared handle, not a back-reference.
@ -215,6 +212,13 @@ export function ChatBar({
stashAt
} = useComposerDraft({ activeQueueSessionKey, focusKey, inputDisabled, queueEditRef, sessionId })
// "Add URL" dialog — open/value state, autofocus, and submit (host onAddUrl or
// an @url: directive into the draft).
const { openUrlDialog, setUrlOpen, setUrlValue, submitUrl, urlInputRef, urlOpen, urlValue } = useComposerUrlDialog({
insertText,
onAddUrl
})
// The queue engine — queued turns, in-place editing, the shared drain lock,
// and bounded auto-drain. Consumes the draft API and writes `queueEditRef`.
const {
@ -323,12 +327,6 @@ export function ChatBar({
: t.composer.placeholderStarting
: restingPlaceholder
useEffect(() => {
if (urlOpen) {
window.requestAnimationFrame(() => urlInputRef.current?.focus({ preventScroll: true }))
}
}, [urlOpen])
// Keep the floating box on-screen: re-clamp (with the real measured size +
// thread bounds) when it pops out and on every window resize — so a position
// persisted on a bigger/other monitor, a shrunk window, or now-wider sidebar
@ -975,24 +973,6 @@ export function ChatBar({
// Global Esc-to-cancel when the chat (not the composer input) has focus.
useComposerEscCancel({ awaitingInput, busy, onCancel })
const submitUrl = () => {
const url = urlValue.trim()
if (!url) {
return
}
if (onAddUrl) {
onAddUrl(url)
} else {
insertText(`@url:${url}`)
}
triggerHaptic('success')
setUrlValue('')
setUrlOpen(false)
}
const {
conversation,
dictate,
@ -1017,10 +997,7 @@ export function ChatBar({
const contextMenu = (
<ContextMenu
onInsertText={insertText}
onOpenUrlDialog={() => {
triggerHaptic('open')
setUrlOpen(true)
}}
onOpenUrlDialog={openUrlDialog}
onPasteClipboardImage={onPasteClipboardImage}
onPickFiles={onPickFiles}
onPickFolders={onPickFolders}