diff --git a/web/src/components/Studio.jsx b/web/src/components/Studio.jsx index dfc3028..abd5398 100644 --- a/web/src/components/Studio.jsx +++ b/web/src/components/Studio.jsx @@ -329,6 +329,20 @@ export default function Studio({ api }) { messagesEnd.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, streaming, streamThinking, streamToolCalls]) + useEffect(() => { + const onTab = (e) => { + if (e.key !== 'Tab') return + if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return + const feed = document.querySelector('.studio-feed-layout') + if (!feed?.closest('.tab-hidden')) { + e.preventDefault() + textareaRef.current?.focus() + } + } + window.addEventListener('keydown', onTab) + return () => window.removeEventListener('keydown', onTab) + }, []) + useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto' @@ -537,10 +551,38 @@ export default function Studio({ api }) { } }, []) + const COMMANDS = ['/clear', '/summarize', '/help', '/plan', '/export', '/model'] + const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() + return + } + if (e.key === 'Tab') { + e.preventDefault() + const ta = textareaRef.current + if (!ta) return + if (document.activeElement !== ta) { + ta.focus() + return + } + const val = ta.value + const pos = ta.selectionStart + const before = val.slice(0, pos) + const afterSlash = before.match(/\/(\w*)$/) + if (afterSlash) { + const partial = afterSlash[0] + const matches = COMMANDS.filter(c => c.startsWith(partial) && c !== partial) + if (matches.length === 1) { + const completed = matches[0] + ' ' + const newText = val.slice(0, pos - afterSlash[0].length) + completed + val.slice(pos) + setInput(newText) + requestAnimationFrame(() => { + ta.selectionStart = ta.selectionEnd = pos - afterSlash[0].length + completed.length + }) + } + } } }