mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-28 19:05:31 +02:00
enhance App and Chat components with improved state management for chat tabs, scroll position preservation, and active state handling; introduce new props for tool management and chat input behavior
This commit is contained in:
parent
6495a1132a
commit
df32d3f822
5 changed files with 417 additions and 223 deletions
|
|
@ -458,6 +458,8 @@ function ContentHeader({
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
type ShortcutPane = 'left' | 'right'
|
||||||
|
|
||||||
// File browser state (for Knowledge section)
|
// File browser state (for Knowledge section)
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||||
const [fileContent, setFileContent] = useState<string>('')
|
const [fileContent, setFileContent] = useState<string>('')
|
||||||
|
|
@ -478,6 +480,7 @@ function App() {
|
||||||
const [graphError, setGraphError] = useState<string | null>(null)
|
const [graphError, setGraphError] = useState<string | null>(null)
|
||||||
const [isChatSidebarOpen, setIsChatSidebarOpen] = useState(true)
|
const [isChatSidebarOpen, setIsChatSidebarOpen] = useState(true)
|
||||||
const [isRightPaneMaximized, setIsRightPaneMaximized] = useState(false)
|
const [isRightPaneMaximized, setIsRightPaneMaximized] = useState(false)
|
||||||
|
const [activeShortcutPane, setActiveShortcutPane] = useState<ShortcutPane>('left')
|
||||||
const isMac = typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac')
|
const isMac = typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac')
|
||||||
const collapsedLeftPaddingPx =
|
const collapsedLeftPaddingPx =
|
||||||
(isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0) +
|
(isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0) +
|
||||||
|
|
@ -534,16 +537,55 @@ function App() {
|
||||||
const chatTabIdCounterRef = useRef(0)
|
const chatTabIdCounterRef = useRef(0)
|
||||||
const newChatTabId = () => `chat-tab-${++chatTabIdCounterRef.current}`
|
const newChatTabId = () => `chat-tab-${++chatTabIdCounterRef.current}`
|
||||||
const chatDraftsRef = useRef(new Map<string, string>())
|
const chatDraftsRef = useRef(new Map<string, string>())
|
||||||
|
const chatScrollTopByTabRef = useRef(new Map<string, number>())
|
||||||
|
const [toolOpenByTab, setToolOpenByTab] = useState<Record<string, Record<string, boolean>>>({})
|
||||||
const activeChatTabIdRef = useRef(activeChatTabId)
|
const activeChatTabIdRef = useRef(activeChatTabId)
|
||||||
activeChatTabIdRef.current = activeChatTabId
|
activeChatTabIdRef.current = activeChatTabId
|
||||||
const handleDraftChange = useCallback((text: string) => {
|
const setChatDraftForTab = useCallback((tabId: string, text: string) => {
|
||||||
const tabId = activeChatTabIdRef.current
|
|
||||||
if (text) {
|
if (text) {
|
||||||
chatDraftsRef.current.set(tabId, text)
|
chatDraftsRef.current.set(tabId, text)
|
||||||
} else {
|
} else {
|
||||||
chatDraftsRef.current.delete(tabId)
|
chatDraftsRef.current.delete(tabId)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
const isToolOpenForTab = useCallback((tabId: string, toolId: string): boolean => {
|
||||||
|
return toolOpenByTab[tabId]?.[toolId] ?? false
|
||||||
|
}, [toolOpenByTab])
|
||||||
|
const setToolOpenForTab = useCallback((tabId: string, toolId: string, open: boolean) => {
|
||||||
|
setToolOpenByTab((prev) => {
|
||||||
|
const prevForTab = prev[tabId] ?? {}
|
||||||
|
if (prevForTab[toolId] === open) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[tabId]: {
|
||||||
|
...prevForTab,
|
||||||
|
[toolId]: open,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const getChatScrollContainer = useCallback((tabId: string): HTMLElement | null => {
|
||||||
|
if (typeof document === 'undefined') return null
|
||||||
|
const panel = document.querySelector<HTMLElement>(
|
||||||
|
`[data-chat-tab-panel="${tabId}"][aria-hidden="false"]`
|
||||||
|
)
|
||||||
|
if (!panel) return null
|
||||||
|
const logRoot = panel.querySelector<HTMLElement>('[role="log"]')
|
||||||
|
if (!logRoot) return null
|
||||||
|
const children = Array.from(logRoot.children) as HTMLElement[]
|
||||||
|
for (const child of children) {
|
||||||
|
const style = window.getComputedStyle(child)
|
||||||
|
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [])
|
||||||
|
const saveChatScrollForTab = useCallback((tabId: string) => {
|
||||||
|
const container = getChatScrollContainer(tabId)
|
||||||
|
if (!container) return
|
||||||
|
chatScrollTopByTabRef.current.set(tabId, container.scrollTop)
|
||||||
|
}, [getChatScrollContainer])
|
||||||
|
|
||||||
const getChatTabTitle = useCallback((tab: ChatTab) => {
|
const getChatTabTitle = useCallback((tab: ChatTab) => {
|
||||||
if (!tab.runId) return 'New chat'
|
if (!tab.runId) return 'New chat'
|
||||||
|
|
@ -1701,6 +1743,7 @@ function App() {
|
||||||
const tab = chatTabs.find(t => t.id === tabId)
|
const tab = chatTabs.find(t => t.id === tabId)
|
||||||
if (!tab) return
|
if (!tab) return
|
||||||
if (tabId === activeChatTabId) return
|
if (tabId === activeChatTabId) return
|
||||||
|
saveChatScrollForTab(activeChatTabId)
|
||||||
setActiveChatTabId(tabId)
|
setActiveChatTabId(tabId)
|
||||||
const restored = restoreChatTabState(tabId, tab.runId)
|
const restored = restoreChatTabState(tabId, tab.runId)
|
||||||
if (tab.runId && processingRunIdsRef.current.has(tab.runId)) {
|
if (tab.runId && processingRunIdsRef.current.has(tab.runId)) {
|
||||||
|
|
@ -1710,12 +1753,13 @@ function App() {
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
applyChatTab(tab)
|
applyChatTab(tab)
|
||||||
}
|
}
|
||||||
}, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState])
|
}, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState, saveChatScrollForTab])
|
||||||
|
|
||||||
const closeChatTab = useCallback((tabId: string) => {
|
const closeChatTab = useCallback((tabId: string) => {
|
||||||
if (chatTabs.length <= 1) return
|
if (chatTabs.length <= 1) return
|
||||||
const idx = chatTabs.findIndex(t => t.id === tabId)
|
const idx = chatTabs.findIndex(t => t.id === tabId)
|
||||||
if (idx === -1) return
|
if (idx === -1) return
|
||||||
|
saveChatScrollForTab(tabId)
|
||||||
const nextTabs = chatTabs.filter(t => t.id !== tabId)
|
const nextTabs = chatTabs.filter(t => t.id !== tabId)
|
||||||
setChatTabs(nextTabs)
|
setChatTabs(nextTabs)
|
||||||
setChatViewStateByTab(prev => {
|
setChatViewStateByTab(prev => {
|
||||||
|
|
@ -1725,6 +1769,13 @@ function App() {
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
chatDraftsRef.current.delete(tabId)
|
chatDraftsRef.current.delete(tabId)
|
||||||
|
chatScrollTopByTabRef.current.delete(tabId)
|
||||||
|
setToolOpenByTab((prev) => {
|
||||||
|
if (!(tabId in prev)) return prev
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[tabId]
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
if (tabId === activeChatTabId && nextTabs.length > 0) {
|
if (tabId === activeChatTabId && nextTabs.length > 0) {
|
||||||
const newIdx = Math.min(idx, nextTabs.length - 1)
|
const newIdx = Math.min(idx, nextTabs.length - 1)
|
||||||
|
|
@ -1737,7 +1788,81 @@ function App() {
|
||||||
applyChatTab(newActiveTab)
|
applyChatTab(newActiveTab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState])
|
}, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState, saveChatScrollForTab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cleanupScrollListener: (() => void) | undefined
|
||||||
|
let pollRaf: number | undefined
|
||||||
|
let restoreRafA: number | undefined
|
||||||
|
let restoreRafB: number | undefined
|
||||||
|
let restoreTimeout: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const restoreScrollTop = (container: HTMLElement, top: number) => {
|
||||||
|
const maxScroll = Math.max(0, container.scrollHeight - container.clientHeight)
|
||||||
|
const clampedTop = clampNumber(top, 0, maxScroll)
|
||||||
|
container.scrollTop = clampedTop
|
||||||
|
}
|
||||||
|
|
||||||
|
const attach = (): boolean => {
|
||||||
|
if (cancelled) return true
|
||||||
|
const container = getChatScrollContainer(activeChatTabId)
|
||||||
|
if (!container) return false
|
||||||
|
|
||||||
|
const savedTop = chatScrollTopByTabRef.current.get(activeChatTabId)
|
||||||
|
if (savedTop !== undefined) {
|
||||||
|
// Reinforce restoration across a couple frames because stick-to-bottom
|
||||||
|
// may schedule scroll adjustments during mount/resize.
|
||||||
|
restoreScrollTop(container, savedTop)
|
||||||
|
restoreRafA = requestAnimationFrame(() => {
|
||||||
|
restoreScrollTop(container, savedTop)
|
||||||
|
restoreRafB = requestAnimationFrame(() => {
|
||||||
|
restoreScrollTop(container, savedTop)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
restoreTimeout = setTimeout(() => {
|
||||||
|
restoreScrollTop(container, savedTop)
|
||||||
|
}, 220)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
chatScrollTopByTabRef.current.set(activeChatTabId, container.scrollTop)
|
||||||
|
}
|
||||||
|
container.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
cleanupScrollListener = () => {
|
||||||
|
chatScrollTopByTabRef.current.set(activeChatTabId, container.scrollTop)
|
||||||
|
container.removeEventListener('scroll', onScroll)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempts = 0
|
||||||
|
const maxAttempts = 60
|
||||||
|
const pollAttach = () => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (attach()) return
|
||||||
|
if (attempts >= maxAttempts) return
|
||||||
|
attempts += 1
|
||||||
|
pollRaf = requestAnimationFrame(pollAttach)
|
||||||
|
}
|
||||||
|
pollAttach()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
cleanupScrollListener?.()
|
||||||
|
if (pollRaf !== undefined) cancelAnimationFrame(pollRaf)
|
||||||
|
if (restoreRafA !== undefined) cancelAnimationFrame(restoreRafA)
|
||||||
|
if (restoreRafB !== undefined) cancelAnimationFrame(restoreRafB)
|
||||||
|
if (restoreTimeout !== undefined) clearTimeout(restoreTimeout)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
activeChatTabId,
|
||||||
|
selectedPath,
|
||||||
|
isGraphOpen,
|
||||||
|
isChatSidebarOpen,
|
||||||
|
isRightPaneMaximized,
|
||||||
|
getChatScrollContainer,
|
||||||
|
])
|
||||||
|
|
||||||
// File tab operations
|
// File tab operations
|
||||||
const openFileInNewTab = useCallback((path: string) => {
|
const openFileInNewTab = useCallback((path: string) => {
|
||||||
|
|
@ -2086,13 +2211,18 @@ function App() {
|
||||||
const handleTabKeyDown = (e: KeyboardEvent) => {
|
const handleTabKeyDown = (e: KeyboardEvent) => {
|
||||||
const mod = e.metaKey || e.ctrlKey
|
const mod = e.metaKey || e.ctrlKey
|
||||||
if (!mod) return
|
if (!mod) return
|
||||||
const inFileView = Boolean(selectedPath)
|
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen) && isChatSidebarOpen)
|
||||||
|
const targetPane: ShortcutPane = rightPaneAvailable
|
||||||
|
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
||||||
|
: 'left'
|
||||||
|
const inFileView = targetPane === 'left' && Boolean(selectedPath)
|
||||||
|
const targetFileTabId = activeFileTabId ?? fileTabs.find((tab) => tab.path === selectedPath)?.id ?? null
|
||||||
|
|
||||||
// Cmd+W — close active tab
|
// Cmd+W — close active tab
|
||||||
if (e.key === 'w') {
|
if (e.key === 'w') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (inFileView && activeFileTabId) {
|
if (inFileView && targetFileTabId) {
|
||||||
closeFileTab(activeFileTabId)
|
closeFileTab(targetFileTabId)
|
||||||
} else {
|
} else {
|
||||||
closeChatTab(activeChatTabId)
|
closeChatTab(activeChatTabId)
|
||||||
}
|
}
|
||||||
|
|
@ -2120,7 +2250,7 @@ function App() {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const direction = e.key === ']' ? 1 : -1
|
const direction = e.key === ']' ? 1 : -1
|
||||||
if (inFileView) {
|
if (inFileView) {
|
||||||
const currentIdx = fileTabs.findIndex(t => t.id === activeFileTabId)
|
const currentIdx = fileTabs.findIndex(t => t.id === targetFileTabId)
|
||||||
if (currentIdx === -1) return
|
if (currentIdx === -1) return
|
||||||
const nextIdx = (currentIdx + direction + fileTabs.length) % fileTabs.length
|
const nextIdx = (currentIdx + direction + fileTabs.length) % fileTabs.length
|
||||||
switchFileTab(fileTabs[nextIdx].id)
|
switchFileTab(fileTabs[nextIdx].id)
|
||||||
|
|
@ -2135,7 +2265,7 @@ function App() {
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', handleTabKeyDown)
|
document.addEventListener('keydown', handleTabKeyDown)
|
||||||
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
||||||
}, [selectedPath, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
}, [selectedPath, isGraphOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
||||||
|
|
||||||
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
||||||
if (kind === 'file') {
|
if (kind === 'file') {
|
||||||
|
|
@ -2496,7 +2626,7 @@ function App() {
|
||||||
}
|
}
|
||||||
}, [isGraphOpen, knowledgeFilePaths])
|
}, [isGraphOpen, knowledgeFilePaths])
|
||||||
|
|
||||||
const renderConversationItem = (item: ConversationItem) => {
|
const renderConversationItem = (item: ConversationItem, tabId: string) => {
|
||||||
if (isChatMessage(item)) {
|
if (isChatMessage(item)) {
|
||||||
if (item.role === 'user') {
|
if (item.role === 'user') {
|
||||||
const { message, files } = parseAttachedFiles(item.content)
|
const { message, files } = parseAttachedFiles(item.content)
|
||||||
|
|
@ -2567,7 +2697,11 @@ function App() {
|
||||||
const output = normalizeToolOutput(item.result, item.status)
|
const output = normalizeToolOutput(item.result, item.status)
|
||||||
const input = normalizeToolInput(item.input)
|
const input = normalizeToolInput(item.input)
|
||||||
return (
|
return (
|
||||||
<Tool key={item.id}>
|
<Tool
|
||||||
|
key={item.id}
|
||||||
|
open={isToolOpenForTab(tabId, item.id)}
|
||||||
|
onOpenChange={(open) => setToolOpenForTab(tabId, item.id, open)}
|
||||||
|
>
|
||||||
<ToolHeader
|
<ToolHeader
|
||||||
title={item.name}
|
title={item.name}
|
||||||
type={`tool-${item.name}`}
|
type={`tool-${item.name}`}
|
||||||
|
|
@ -2719,7 +2853,11 @@ function App() {
|
||||||
selectedBackgroundTask={selectedBackgroundTask}
|
selectedBackgroundTask={selectedBackgroundTask}
|
||||||
/>
|
/>
|
||||||
{!isRightPaneOnlyMode && (
|
{!isRightPaneOnlyMode && (
|
||||||
<SidebarInset className="overflow-hidden! min-h-0 min-w-0">
|
<SidebarInset
|
||||||
|
className="overflow-hidden! min-h-0 min-w-0"
|
||||||
|
onMouseDownCapture={() => setActiveShortcutPane('left')}
|
||||||
|
onFocusCapture={() => setActiveShortcutPane('left')}
|
||||||
|
>
|
||||||
{/* Header - also serves as titlebar drag region, adjusts padding when sidebar collapsed */}
|
{/* Header - also serves as titlebar drag region, adjusts padding when sidebar collapsed */}
|
||||||
<ContentHeader
|
<ContentHeader
|
||||||
onNavigateBack={() => { void navigateBack() }}
|
onNavigateBack={() => { void navigateBack() }}
|
||||||
|
|
@ -2881,6 +3019,7 @@ function App() {
|
||||||
) : (
|
) : (
|
||||||
<FileCardProvider onOpenKnowledgeFile={(path) => { navigateToFile(path) }}>
|
<FileCardProvider onOpenKnowledgeFile={(path) => { navigateToFile(path) }}>
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
|
<div className="relative min-h-0 flex-1">
|
||||||
{chatTabs.map((tab) => {
|
{chatTabs.map((tab) => {
|
||||||
const isActive = tab.id === activeChatTabId
|
const isActive = tab.id === activeChatTabId
|
||||||
const tabState = getChatTabStateForRender(tab.id)
|
const tabState = getChatTabStateForRender(tab.id)
|
||||||
|
|
@ -2891,7 +3030,12 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={cn('min-h-0 flex-1 flex-col', isActive ? 'flex' : 'hidden')}
|
className={cn(
|
||||||
|
'min-h-0 h-full flex-col',
|
||||||
|
isActive
|
||||||
|
? 'flex'
|
||||||
|
: 'pointer-events-none invisible absolute inset-0 flex'
|
||||||
|
)}
|
||||||
data-chat-tab-panel={tab.id}
|
data-chat-tab-panel={tab.id}
|
||||||
aria-hidden={!isActive}
|
aria-hidden={!isActive}
|
||||||
>
|
>
|
||||||
|
|
@ -2907,7 +3051,7 @@ function App() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{tabState.conversation.map(item => {
|
{tabState.conversation.map(item => {
|
||||||
const rendered = renderConversationItem(item)
|
const rendered = renderConversationItem(item, tab.id)
|
||||||
if (isToolCall(item)) {
|
if (isToolCall(item)) {
|
||||||
const permRequest = tabState.allPermissionRequests.get(item.id)
|
const permRequest = tabState.allPermissionRequests.get(item.id)
|
||||||
if (permRequest) {
|
if (permRequest) {
|
||||||
|
|
@ -2960,6 +3104,7 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="sticky bottom-0 z-10 bg-background pb-12 pt-0 shadow-lg">
|
<div className="sticky bottom-0 z-10 bg-background pb-12 pt-0 shadow-lg">
|
||||||
<div className="pointer-events-none absolute inset-x-0 -top-6 h-6 bg-linear-to-t from-background to-transparent" />
|
<div className="pointer-events-none absolute inset-x-0 -top-6 h-6 bg-linear-to-t from-background to-transparent" />
|
||||||
|
|
@ -2967,22 +3112,35 @@ function App() {
|
||||||
{!hasConversation && (
|
{!hasConversation && (
|
||||||
<Suggestions onSelect={setPresetMessage} className="mb-3 justify-center" />
|
<Suggestions onSelect={setPresetMessage} className="mb-3 justify-center" />
|
||||||
)}
|
)}
|
||||||
|
{chatTabs.map((tab) => {
|
||||||
|
const isActive = tab.id === activeChatTabId
|
||||||
|
const tabState = getChatTabStateForRender(tab.id)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className={isActive ? 'block' : 'hidden'}
|
||||||
|
data-chat-input-panel={tab.id}
|
||||||
|
aria-hidden={!isActive}
|
||||||
|
>
|
||||||
<ChatInputWithMentions
|
<ChatInputWithMentions
|
||||||
key={activeChatTabId}
|
|
||||||
knowledgeFiles={knowledgeFiles}
|
knowledgeFiles={knowledgeFiles}
|
||||||
recentFiles={recentWikiFiles}
|
recentFiles={recentWikiFiles}
|
||||||
visibleFiles={visibleKnowledgeFiles}
|
visibleFiles={visibleKnowledgeFiles}
|
||||||
onSubmit={handlePromptSubmit}
|
onSubmit={handlePromptSubmit}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
isProcessing={isProcessing}
|
isProcessing={isActive && isProcessing}
|
||||||
isStopping={isStopping}
|
isStopping={isActive && isStopping}
|
||||||
presetMessage={presetMessage}
|
isActive={isActive}
|
||||||
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
presetMessage={isActive ? presetMessage : undefined}
|
||||||
runId={runId}
|
onPresetMessageConsumed={isActive ? () => setPresetMessage(undefined) : undefined}
|
||||||
initialDraft={chatDraftsRef.current.get(activeChatTabId)}
|
runId={tabState.runId}
|
||||||
onDraftChange={handleDraftChange}
|
initialDraft={chatDraftsRef.current.get(tab.id)}
|
||||||
|
onDraftChange={(text) => setChatDraftForTab(tab.id, text)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FileCardProvider>
|
</FileCardProvider>
|
||||||
|
|
@ -3017,14 +3175,17 @@ function App() {
|
||||||
runId={runId}
|
runId={runId}
|
||||||
presetMessage={presetMessage}
|
presetMessage={presetMessage}
|
||||||
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
||||||
initialDraft={chatDraftsRef.current.get(activeChatTabId)}
|
getInitialDraft={(tabId) => chatDraftsRef.current.get(tabId)}
|
||||||
onDraftChange={handleDraftChange}
|
onDraftChangeForTab={setChatDraftForTab}
|
||||||
pendingAskHumanRequests={pendingAskHumanRequests}
|
pendingAskHumanRequests={pendingAskHumanRequests}
|
||||||
allPermissionRequests={allPermissionRequests}
|
allPermissionRequests={allPermissionRequests}
|
||||||
permissionResponses={permissionResponses}
|
permissionResponses={permissionResponses}
|
||||||
onPermissionResponse={handlePermissionResponse}
|
onPermissionResponse={handlePermissionResponse}
|
||||||
onAskHumanResponse={handleAskHumanResponse}
|
onAskHumanResponse={handleAskHumanResponse}
|
||||||
|
isToolOpenForTab={isToolOpenForTab}
|
||||||
|
onToolOpenChangeForTab={setToolOpenForTab}
|
||||||
onOpenKnowledgeFile={(path) => { navigateToFile(path) }}
|
onOpenKnowledgeFile={(path) => { navigateToFile(path) }}
|
||||||
|
onActivate={() => setActiveShortcutPane('right')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Rendered last so its no-drag region paints over the sidebar drag region */}
|
{/* Rendered last so its no-drag region paints over the sidebar drag region */}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export const Conversation = ({ className, children, ...props }: ConversationProp
|
||||||
* Must be used inside Conversation component.
|
* Must be used inside Conversation component.
|
||||||
*/
|
*/
|
||||||
export const ScrollPositionPreserver = () => {
|
export const ScrollPositionPreserver = () => {
|
||||||
const { isAtBottom } = useStickToBottomContext();
|
const { isAtBottom, scrollRef } = useStickToBottomContext();
|
||||||
const preservationContext = useContext(ScrollPreservationContext);
|
const preservationContext = useContext(ScrollPreservationContext);
|
||||||
const containerFoundRef = useRef(false);
|
const containerFoundRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -110,29 +110,13 @@ export const ScrollPositionPreserver = () => {
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (containerFoundRef.current || !preservationContext) return;
|
if (containerFoundRef.current || !preservationContext) return;
|
||||||
|
|
||||||
// Find the scroll container (StickToBottom creates one)
|
// Use the local StickToBottom scroll container for this conversation instance.
|
||||||
// It's the first parent with overflow-y scroll/auto
|
const container = scrollRef.current;
|
||||||
const findScrollContainer = (): HTMLElement | null => {
|
|
||||||
const candidates = document.querySelectorAll('[role="log"]');
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
// The scroll container is a direct child of the role="log" element
|
|
||||||
const children = candidate.children;
|
|
||||||
for (const child of children) {
|
|
||||||
const style = window.getComputedStyle(child);
|
|
||||||
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
|
|
||||||
return child as HTMLElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const container = findScrollContainer();
|
|
||||||
if (container) {
|
if (container) {
|
||||||
preservationContext.registerScrollContainer(container);
|
preservationContext.registerScrollContainer(container);
|
||||||
containerFoundRef.current = true;
|
containerFoundRef.current = true;
|
||||||
}
|
}
|
||||||
}, [preservationContext]);
|
}, [preservationContext, scrollRef]);
|
||||||
|
|
||||||
// Track engagement based on scroll position
|
// Track engagement based on scroll position
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -931,7 +931,13 @@ export const PromptInputTextarea = ({
|
||||||
if (autoFocus || focusTrigger !== undefined) {
|
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();
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) return;
|
||||||
|
try {
|
||||||
|
textarea.focus({ preventScroll: true });
|
||||||
|
} catch {
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface ChatInputInnerProps {
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
isStopping?: boolean
|
isStopping?: boolean
|
||||||
|
isActive: boolean
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
runId?: string | null
|
runId?: string | null
|
||||||
|
|
@ -28,6 +29,7 @@ function ChatInputInner({
|
||||||
onStop,
|
onStop,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
isStopping,
|
isStopping,
|
||||||
|
isActive,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
runId,
|
runId,
|
||||||
|
|
@ -72,6 +74,7 @@ function ChatInputInner({
|
||||||
}, [handleSubmit])
|
}, [handleSubmit])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isActive) return
|
||||||
const onDragOver = (e: DragEvent) => {
|
const onDragOver = (e: DragEvent) => {
|
||||||
if (e.dataTransfer?.types?.includes('Files')) {
|
if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -100,15 +103,15 @@ function ChatInputInner({
|
||||||
document.removeEventListener('dragover', onDragOver)
|
document.removeEventListener('dragover', onDragOver)
|
||||||
document.removeEventListener('drop', onDrop)
|
document.removeEventListener('drop', onDrop)
|
||||||
}
|
}
|
||||||
}, [controller])
|
}, [controller, isActive])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-4 shadow-none">
|
<div className="flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-4 shadow-none">
|
||||||
<PromptInputTextarea
|
<PromptInputTextarea
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
autoFocus
|
autoFocus={isActive}
|
||||||
focusTrigger={runId}
|
focusTrigger={isActive ? runId : undefined}
|
||||||
className="min-h-6 rounded-none border-0 py-0 shadow-none focus-visible:ring-0"
|
className="min-h-6 rounded-none border-0 py-0 shadow-none focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
|
|
@ -156,6 +159,7 @@ export interface ChatInputWithMentionsProps {
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
isStopping?: boolean
|
isStopping?: boolean
|
||||||
|
isActive?: boolean
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
runId?: string | null
|
runId?: string | null
|
||||||
|
|
@ -171,6 +175,7 @@ export function ChatInputWithMentions({
|
||||||
onStop,
|
onStop,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
isStopping,
|
isStopping,
|
||||||
|
isActive = true,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
runId,
|
runId,
|
||||||
|
|
@ -184,6 +189,7 @@ export function ChatInputWithMentions({
|
||||||
onStop={onStop}
|
onStop={onStop}
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
isStopping={isStopping}
|
isStopping={isStopping}
|
||||||
|
isActive={isActive}
|
||||||
presetMessage={presetMessage}
|
presetMessage={presetMessage}
|
||||||
onPresetMessageConsumed={onPresetMessageConsumed}
|
onPresetMessageConsumed={onPresetMessageConsumed}
|
||||||
runId={runId}
|
runId={runId}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ interface ErrorMessage {
|
||||||
type ConversationItem = ChatMessage | ToolCall | ErrorMessage
|
type ConversationItem = ChatMessage | ToolCall | ErrorMessage
|
||||||
|
|
||||||
type ChatTabViewState = {
|
type ChatTabViewState = {
|
||||||
|
runId: string | null
|
||||||
conversation: ConversationItem[]
|
conversation: ConversationItem[]
|
||||||
currentAssistantMessage: string
|
currentAssistantMessage: string
|
||||||
pendingAskHumanRequests: Map<string, z.infer<typeof AskHumanRequestEvent>>
|
pendingAskHumanRequests: Map<string, z.infer<typeof AskHumanRequestEvent>>
|
||||||
|
|
@ -160,14 +161,17 @@ interface ChatSidebarProps {
|
||||||
runId?: string | null
|
runId?: string | null
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
initialDraft?: string
|
getInitialDraft?: (tabId: string) => string | undefined
|
||||||
onDraftChange?: (text: string) => void
|
onDraftChangeForTab?: (tabId: string, text: string) => void
|
||||||
pendingAskHumanRequests?: Map<string, z.infer<typeof AskHumanRequestEvent>>
|
pendingAskHumanRequests?: Map<string, z.infer<typeof AskHumanRequestEvent>>
|
||||||
allPermissionRequests?: Map<string, z.infer<typeof ToolPermissionRequestEvent>>
|
allPermissionRequests?: Map<string, z.infer<typeof ToolPermissionRequestEvent>>
|
||||||
permissionResponses?: Map<string, 'approve' | 'deny'>
|
permissionResponses?: Map<string, 'approve' | 'deny'>
|
||||||
onPermissionResponse?: (toolCallId: string, subflow: string[], response: 'approve' | 'deny') => void
|
onPermissionResponse?: (toolCallId: string, subflow: string[], response: 'approve' | 'deny') => void
|
||||||
onAskHumanResponse?: (toolCallId: string, subflow: string[], response: string) => void
|
onAskHumanResponse?: (toolCallId: string, subflow: string[], response: string) => void
|
||||||
|
isToolOpenForTab?: (tabId: string, toolId: string) => boolean
|
||||||
|
onToolOpenChangeForTab?: (tabId: string, toolId: string, open: boolean) => void
|
||||||
onOpenKnowledgeFile?: (path: string) => void
|
onOpenKnowledgeFile?: (path: string) => void
|
||||||
|
onActivate?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatSidebar({
|
export function ChatSidebar({
|
||||||
|
|
@ -195,14 +199,17 @@ export function ChatSidebar({
|
||||||
runId,
|
runId,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
initialDraft,
|
getInitialDraft,
|
||||||
onDraftChange,
|
onDraftChangeForTab,
|
||||||
pendingAskHumanRequests = new Map(),
|
pendingAskHumanRequests = new Map(),
|
||||||
allPermissionRequests = new Map(),
|
allPermissionRequests = new Map(),
|
||||||
permissionResponses = new Map(),
|
permissionResponses = new Map(),
|
||||||
onPermissionResponse,
|
onPermissionResponse,
|
||||||
onAskHumanResponse,
|
onAskHumanResponse,
|
||||||
|
isToolOpenForTab,
|
||||||
|
onToolOpenChangeForTab,
|
||||||
onOpenKnowledgeFile,
|
onOpenKnowledgeFile,
|
||||||
|
onActivate,
|
||||||
}: ChatSidebarProps) {
|
}: ChatSidebarProps) {
|
||||||
const [width, setWidth] = useState(() => getInitialPaneWidth(defaultWidth))
|
const [width, setWidth] = useState(() => getInitialPaneWidth(defaultWidth))
|
||||||
const [isResizing, setIsResizing] = useState(false)
|
const [isResizing, setIsResizing] = useState(false)
|
||||||
|
|
@ -284,12 +291,14 @@ export function ChatSidebar({
|
||||||
}, [width, getMaxAllowedWidth])
|
}, [width, getMaxAllowedWidth])
|
||||||
|
|
||||||
const activeTabState = useMemo<ChatTabViewState>(() => ({
|
const activeTabState = useMemo<ChatTabViewState>(() => ({
|
||||||
|
runId: runId ?? null,
|
||||||
conversation,
|
conversation,
|
||||||
currentAssistantMessage,
|
currentAssistantMessage,
|
||||||
pendingAskHumanRequests,
|
pendingAskHumanRequests,
|
||||||
allPermissionRequests,
|
allPermissionRequests,
|
||||||
permissionResponses,
|
permissionResponses,
|
||||||
}), [
|
}), [
|
||||||
|
runId,
|
||||||
conversation,
|
conversation,
|
||||||
currentAssistantMessage,
|
currentAssistantMessage,
|
||||||
pendingAskHumanRequests,
|
pendingAskHumanRequests,
|
||||||
|
|
@ -297,6 +306,7 @@ export function ChatSidebar({
|
||||||
permissionResponses,
|
permissionResponses,
|
||||||
])
|
])
|
||||||
const emptyTabState = useMemo<ChatTabViewState>(() => ({
|
const emptyTabState = useMemo<ChatTabViewState>(() => ({
|
||||||
|
runId: null,
|
||||||
conversation: [],
|
conversation: [],
|
||||||
currentAssistantMessage: '',
|
currentAssistantMessage: '',
|
||||||
pendingAskHumanRequests: new Map(),
|
pendingAskHumanRequests: new Map(),
|
||||||
|
|
@ -309,7 +319,7 @@ export function ChatSidebar({
|
||||||
}, [activeChatTabId, activeTabState, chatTabStates, emptyTabState])
|
}, [activeChatTabId, activeTabState, chatTabStates, emptyTabState])
|
||||||
const hasConversation = activeTabState.conversation.length > 0 || Boolean(activeTabState.currentAssistantMessage)
|
const hasConversation = activeTabState.conversation.length > 0 || Boolean(activeTabState.currentAssistantMessage)
|
||||||
|
|
||||||
const renderConversationItem = (item: ConversationItem) => {
|
const renderConversationItem = (item: ConversationItem, tabId: string) => {
|
||||||
if (isChatMessage(item)) {
|
if (isChatMessage(item)) {
|
||||||
return (
|
return (
|
||||||
<Message key={item.id} from={item.role}>
|
<Message key={item.id} from={item.role}>
|
||||||
|
|
@ -329,7 +339,11 @@ export function ChatSidebar({
|
||||||
const output = normalizeToolOutput(item.result, item.status)
|
const output = normalizeToolOutput(item.result, item.status)
|
||||||
const input = normalizeToolInput(item.input)
|
const input = normalizeToolInput(item.input)
|
||||||
return (
|
return (
|
||||||
<Tool key={item.id}>
|
<Tool
|
||||||
|
key={item.id}
|
||||||
|
open={isToolOpenForTab?.(tabId, item.id) ?? false}
|
||||||
|
onOpenChange={(open) => onToolOpenChangeForTab?.(tabId, item.id, open)}
|
||||||
|
>
|
||||||
<ToolHeader title={item.name} type={`tool-${item.name}`} state={toToolState(item.status)} />
|
<ToolHeader title={item.name} type={`tool-${item.name}`} state={toToolState(item.status)} />
|
||||||
<ToolContent>
|
<ToolContent>
|
||||||
<ToolInput input={input} />
|
<ToolInput input={input} />
|
||||||
|
|
@ -367,6 +381,8 @@ export function ChatSidebar({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={paneRef}
|
ref={paneRef}
|
||||||
|
onMouseDownCapture={onActivate}
|
||||||
|
onFocusCapture={onActivate}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex min-w-0 flex-col overflow-hidden border-l border-border bg-background',
|
'relative flex min-w-0 flex-col overflow-hidden border-l border-border bg-background',
|
||||||
!isResizing && 'transition-[width] duration-200 ease-linear'
|
!isResizing && 'transition-[width] duration-200 ease-linear'
|
||||||
|
|
@ -418,12 +434,13 @@ export function ChatSidebar({
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onOpenFullScreen}
|
onClick={onOpenFullScreen}
|
||||||
className="titlebar-no-drag my-1 mr-2 h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
className="titlebar-no-drag my-1 mr-2 h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
|
aria-label={isMaximized ? 'Restore two-pane view' : 'Maximize chat view'}
|
||||||
>
|
>
|
||||||
{isMaximized ? <Shrink className="size-5" /> : <Expand className="size-5" />}
|
{isMaximized ? <Shrink className="size-5" /> : <Expand className="size-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
{isMaximized ? 'Restore two-pane view' : 'Maximize right pane'}
|
{isMaximized ? 'Restore two-pane view' : 'Maximize chat view'}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -431,6 +448,7 @@ export function ChatSidebar({
|
||||||
|
|
||||||
<FileCardProvider onOpenKnowledgeFile={onOpenKnowledgeFile ?? (() => {})}>
|
<FileCardProvider onOpenKnowledgeFile={onOpenKnowledgeFile ?? (() => {})}>
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
|
<div className="relative min-h-0 flex-1">
|
||||||
{chatTabs.map((tab) => {
|
{chatTabs.map((tab) => {
|
||||||
const isActive = tab.id === activeChatTabId
|
const isActive = tab.id === activeChatTabId
|
||||||
const tabState = getTabState(tab.id)
|
const tabState = getTabState(tab.id)
|
||||||
|
|
@ -438,7 +456,12 @@ export function ChatSidebar({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={cn('min-h-0 flex-1 flex-col', isActive ? 'flex' : 'hidden')}
|
className={cn(
|
||||||
|
'min-h-0 h-full flex-col',
|
||||||
|
isActive
|
||||||
|
? 'flex'
|
||||||
|
: 'pointer-events-none invisible absolute inset-0 flex'
|
||||||
|
)}
|
||||||
data-chat-tab-panel={tab.id}
|
data-chat-tab-panel={tab.id}
|
||||||
aria-hidden={!isActive}
|
aria-hidden={!isActive}
|
||||||
>
|
>
|
||||||
|
|
@ -452,7 +475,7 @@ export function ChatSidebar({
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{tabState.conversation.map((item) => {
|
{tabState.conversation.map((item) => {
|
||||||
const rendered = renderConversationItem(item)
|
const rendered = renderConversationItem(item, tab.id)
|
||||||
if (isToolCall(item) && onPermissionResponse) {
|
if (isToolCall(item) && onPermissionResponse) {
|
||||||
const permRequest = tabState.allPermissionRequests.get(item.id)
|
const permRequest = tabState.allPermissionRequests.get(item.id)
|
||||||
if (permRequest) {
|
if (permRequest) {
|
||||||
|
|
@ -505,6 +528,7 @@ export function ChatSidebar({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="sticky bottom-0 z-10 bg-background pb-12 pt-0 shadow-lg">
|
<div className="sticky bottom-0 z-10 bg-background pb-12 pt-0 shadow-lg">
|
||||||
<div className="pointer-events-none absolute inset-x-0 -top-6 h-6 bg-linear-to-t from-background to-transparent" />
|
<div className="pointer-events-none absolute inset-x-0 -top-6 h-6 bg-linear-to-t from-background to-transparent" />
|
||||||
|
|
@ -512,25 +536,38 @@ export function ChatSidebar({
|
||||||
{!hasConversation && (
|
{!hasConversation && (
|
||||||
<Suggestions onSelect={setLocalPresetMessage} className="mb-3 justify-center" />
|
<Suggestions onSelect={setLocalPresetMessage} className="mb-3 justify-center" />
|
||||||
)}
|
)}
|
||||||
|
{chatTabs.map((tab) => {
|
||||||
|
const isActive = tab.id === activeChatTabId
|
||||||
|
const tabState = getTabState(tab.id)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className={isActive ? 'block' : 'hidden'}
|
||||||
|
data-chat-input-panel={tab.id}
|
||||||
|
aria-hidden={!isActive}
|
||||||
|
>
|
||||||
<ChatInputWithMentions
|
<ChatInputWithMentions
|
||||||
key={activeChatTabId}
|
|
||||||
knowledgeFiles={knowledgeFiles}
|
knowledgeFiles={knowledgeFiles}
|
||||||
recentFiles={recentFiles}
|
recentFiles={recentFiles}
|
||||||
visibleFiles={visibleFiles}
|
visibleFiles={visibleFiles}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onStop={onStop}
|
onStop={onStop}
|
||||||
isProcessing={isProcessing}
|
isProcessing={isActive && isProcessing}
|
||||||
isStopping={isStopping}
|
isStopping={isActive && isStopping}
|
||||||
presetMessage={localPresetMessage ?? presetMessage}
|
isActive={isActive}
|
||||||
onPresetMessageConsumed={() => {
|
presetMessage={isActive ? (localPresetMessage ?? presetMessage) : undefined}
|
||||||
|
onPresetMessageConsumed={isActive ? () => {
|
||||||
setLocalPresetMessage(undefined)
|
setLocalPresetMessage(undefined)
|
||||||
onPresetMessageConsumed?.()
|
onPresetMessageConsumed?.()
|
||||||
}}
|
} : undefined}
|
||||||
runId={runId}
|
runId={tabState.runId}
|
||||||
initialDraft={initialDraft}
|
initialDraft={getInitialDraft?.(tab.id)}
|
||||||
onDraftChange={onDraftChange}
|
onDraftChange={onDraftChangeForTab ? (text) => onDraftChangeForTab(tab.id, text) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FileCardProvider>
|
</FileCardProvider>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue