mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
Enhance chat input and sidebar components with runId support and improved auto-focus behavior
This commit is contained in:
parent
f462c558f1
commit
4d2fc01f88
3 changed files with 33 additions and 11 deletions
|
|
@ -282,6 +282,7 @@ interface ChatInputInnerProps {
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
|
runId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatInputInner({
|
function ChatInputInner({
|
||||||
|
|
@ -289,6 +290,7 @@ function ChatInputInner({
|
||||||
isProcessing,
|
isProcessing,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
|
runId,
|
||||||
}: ChatInputInnerProps) {
|
}: ChatInputInnerProps) {
|
||||||
const controller = usePromptInputController()
|
const controller = usePromptInputController()
|
||||||
const message = controller.textInput.value
|
const message = controller.textInput.value
|
||||||
|
|
@ -320,9 +322,9 @@ function ChatInputInner({
|
||||||
<div className="flex items-center gap-2 bg-background border border-border rounded-3xl shadow-xl px-4 py-2.5">
|
<div className="flex items-center gap-2 bg-background border border-border rounded-3xl shadow-xl px-4 py-2.5">
|
||||||
<PromptInputTextarea
|
<PromptInputTextarea
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
disabled={isProcessing}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
focusTrigger={runId}
|
||||||
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
|
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -351,6 +353,7 @@ interface ChatInputWithMentionsProps {
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
|
runId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatInputWithMentions({
|
function ChatInputWithMentions({
|
||||||
|
|
@ -361,6 +364,7 @@ function ChatInputWithMentions({
|
||||||
isProcessing,
|
isProcessing,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
|
runId,
|
||||||
}: ChatInputWithMentionsProps) {
|
}: ChatInputWithMentionsProps) {
|
||||||
return (
|
return (
|
||||||
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
|
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
|
||||||
|
|
@ -369,6 +373,7 @@ function ChatInputWithMentions({
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
presetMessage={presetMessage}
|
presetMessage={presetMessage}
|
||||||
onPresetMessageConsumed={onPresetMessageConsumed}
|
onPresetMessageConsumed={onPresetMessageConsumed}
|
||||||
|
runId={runId}
|
||||||
/>
|
/>
|
||||||
</PromptInputProvider>
|
</PromptInputProvider>
|
||||||
)
|
)
|
||||||
|
|
@ -405,6 +410,7 @@ function App() {
|
||||||
const [currentReasoning, setCurrentReasoning] = useState<string>('')
|
const [currentReasoning, setCurrentReasoning] = useState<string>('')
|
||||||
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
||||||
const [runId, setRunId] = useState<string | null>(null)
|
const [runId, setRunId] = useState<string | null>(null)
|
||||||
|
const runIdRef = useRef<string | null>(null)
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [agentId] = useState<string>('copilot')
|
const [agentId] = useState<string>('copilot')
|
||||||
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
||||||
|
|
@ -427,6 +433,11 @@ function App() {
|
||||||
// Onboarding state
|
// Onboarding state
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||||
|
|
||||||
|
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
|
||||||
|
useEffect(() => {
|
||||||
|
runIdRef.current = runId
|
||||||
|
}, [runId])
|
||||||
|
|
||||||
// Load directory tree
|
// Load directory tree
|
||||||
const loadDirectory = useCallback(async () => {
|
const loadDirectory = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -723,15 +734,17 @@ function App() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Listen to run events
|
// Listen to run events
|
||||||
|
// Listen to run events - use ref to avoid stale closure issues
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
|
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
|
||||||
handleRunEvent(event as RunEventType)
|
handleRunEvent(event as RunEventType)
|
||||||
}) as (event: null) => void)
|
}) as (event: null) => void)
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [runId])
|
}, [])
|
||||||
|
|
||||||
const handleRunEvent = (event: RunEventType) => {
|
const handleRunEvent = (event: RunEventType) => {
|
||||||
if (event.runId !== runId) return
|
// Use ref to get current runId to avoid stale closure issues
|
||||||
|
if (event.runId !== runIdRef.current) return
|
||||||
|
|
||||||
console.log('Run event:', event.type, event)
|
console.log('Run event:', event.type, event)
|
||||||
|
|
||||||
|
|
@ -1044,6 +1057,7 @@ function App() {
|
||||||
setRunId(null)
|
setRunId(null)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
setModelUsage(null)
|
setModelUsage(null)
|
||||||
|
setIsProcessing(false)
|
||||||
setPendingPermissionRequests(new Map())
|
setPendingPermissionRequests(new Map())
|
||||||
setPendingAskHumanRequests(new Map())
|
setPendingAskHumanRequests(new Map())
|
||||||
setAllPermissionRequests(new Map())
|
setAllPermissionRequests(new Map())
|
||||||
|
|
@ -1716,6 +1730,7 @@ function App() {
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
presetMessage={presetMessage}
|
presetMessage={presetMessage}
|
||||||
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
||||||
|
runId={runId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -906,6 +906,7 @@ export type PromptInputTextareaProps = ComponentProps<
|
||||||
typeof InputGroupTextarea
|
typeof InputGroupTextarea
|
||||||
> & {
|
> & {
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
focusTrigger?: unknown; // When this value changes, focus the textarea
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PromptInputTextarea = ({
|
export const PromptInputTextarea = ({
|
||||||
|
|
@ -914,6 +915,7 @@ export const PromptInputTextarea = ({
|
||||||
placeholder = "What would you like to know?",
|
placeholder = "What would you like to know?",
|
||||||
onKeyDown: externalOnKeyDown,
|
onKeyDown: externalOnKeyDown,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
|
focusTrigger,
|
||||||
...props
|
...props
|
||||||
}: PromptInputTextareaProps) => {
|
}: PromptInputTextareaProps) => {
|
||||||
const controller = useOptionalPromptInputController();
|
const controller = useOptionalPromptInputController();
|
||||||
|
|
@ -924,16 +926,16 @@ export const PromptInputTextarea = ({
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
// Auto-focus the textarea when requested
|
// Auto-focus the textarea when requested or when focusTrigger changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus) {
|
if (autoFocus || focusTrigger !== undefined) {
|
||||||
// Small delay to ensure the element is fully mounted and visible
|
// Small delay to ensure the element is fully mounted and visible
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
textareaRef.current?.focus();
|
textareaRef.current?.focus();
|
||||||
}, 50);
|
}, 50);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [autoFocus]);
|
}, [autoFocus, focusTrigger]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const highlightRef = useRef<HTMLDivElement>(null);
|
const highlightRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,10 +260,16 @@ export function ChatSidebar({
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}, [width])
|
}, [width])
|
||||||
|
|
||||||
// Auto-focus textarea when sidebar opens
|
// Auto-focus textarea when sidebar opens or when conversation is cleared (new chat)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
textareaRef.current?.focus()
|
// Focus when conversation is empty (new chat started)
|
||||||
}, [])
|
if (conversation.length === 0) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
}, 50)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [conversation.length])
|
||||||
|
|
||||||
// Auto-populate with @currentfile when switching knowledge files
|
// Auto-populate with @currentfile when switching knowledge files
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -584,9 +590,8 @@ export function ChatSidebar({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onScroll={syncHighlightScroll}
|
onScroll={syncHighlightScroll}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
disabled={isProcessing}
|
|
||||||
rows={1}
|
rows={1}
|
||||||
className="relative z-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:opacity-50 resize-none max-h-32 min-h-6"
|
className="relative z-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground resize-none max-h-32 min-h-6"
|
||||||
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue