diff --git a/adapters/telegram/index.ts b/adapters/telegram/index.ts index a3985169b..a87cf9275 100644 --- a/adapters/telegram/index.ts +++ b/adapters/telegram/index.ts @@ -392,7 +392,11 @@ async function handleServerMessage(chatId: string, msg: ServerMessage): Promise< const text = accumulatedText.get(chatId) if (text?.trim()) { try { - await bot.api.editMessageText(numericChatId, placeholders.get(chatId)!.messageId, text) + const chunks = splitMessage(text, TELEGRAM_TEXT_LIMIT) + await bot.api.editMessageText(numericChatId, placeholders.get(chatId)!.messageId, chunks[0]!) + for (let i = 1; i < chunks.length; i++) { + await bot.api.sendMessage(numericChatId, chunks[i]!) + } } catch { /* ignore */ } } placeholders.delete(chatId) diff --git a/desktop/src/components/chat/AskUserQuestion.tsx b/desktop/src/components/chat/AskUserQuestion.tsx index 7a9670916..28e62140a 100644 --- a/desktop/src/components/chat/AskUserQuestion.tsx +++ b/desktop/src/components/chat/AskUserQuestion.tsx @@ -73,7 +73,7 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) const inputObject = (input && typeof input === 'object') ? input as Record : {} const [activeTab, setActiveTab] = useState(0) const [selections, setSelections] = useState({}) - const [freeText, setFreeText] = useState('') + const [freeTexts, setFreeTexts] = useState>({}) const [hasSubmitted, setHasSubmitted] = useState(false) const composingRef = useRef(false) @@ -95,11 +95,12 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) .filter((answer): answer is string => typeof answer === 'string' && answer.trim().length > 0) .join(', ') } - return freeText.trim() || questions - .map((question, index) => getSelectedAnswer(question, selections[index])) + const textParts = questions.map((_q, i) => freeTexts[i]?.trim()).filter(Boolean) + const selectParts = questions + .map((question, i) => freeTexts[i]?.trim() ? '' : getSelectedAnswer(question, selections[i])) .filter(Boolean) - .join('; ') - }, [freeText, questions, resultAnswers, selections]) + return textParts.join('; ') + (textParts.length > 0 && selectParts.length > 0 ? '; ' : '') + selectParts.join('; ') + }, [freeTexts, questions, resultAnswers, selections]) const submitted = Object.keys(resultAnswers).length > 0 || hasSubmitted const handleSelect = (qIndex: number, label: string) => { @@ -126,7 +127,7 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) } return { ...prev, [qIndex]: [label] } }) - setFreeText('') + setFreeTexts((prev) => { const next = { ...prev }; delete next[qIndex]; return next }) } const handleSubmit = () => { @@ -134,17 +135,23 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) const parts: string[] = [] for (let i = 0; i < questions.length; i++) { - const selected = getSelectedAnswer(questions[i]!, selections[i]) - if (selected) parts.push(selected) + const text = freeTexts[i]?.trim() + if (text) { + parts.push(text) + } else { + const selected = getSelectedAnswer(questions[i]!, selections[i]) + if (selected) parts.push(selected) + } } - const response = freeText.trim() || parts.join('; ') || '' + const response = parts.join('; ') || '' if (!response) return if (!targetSessionId || !pendingRequest) return const answers = questions.reduce>((acc, question, index) => { - if (freeText.trim()) { - acc[question.question] = freeText.trim() + const text = freeTexts[index]?.trim() + if (text) { + acc[question.question] = text } else { const selected = getSelectedAnswer(question, selections[index]) if (selected) acc[question.question] = selected @@ -162,7 +169,9 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) } // All questions must be answered (via selection or free text) to enable submit - const allAnswered = freeText.trim().length > 0 || questions.every((_, i) => (selections[i]?.length ?? 0) > 0) + const allAnswered = questions.every((_, i) => + (freeTexts[i]?.trim()?.length ?? 0) > 0 || (selections[i]?.length ?? 0) > 0 + ) const safeActiveTab = Math.min(activeTab, questions.length - 1) const activeQuestion = questions[safeActiveTab] @@ -202,7 +211,7 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props)
{questions.map((q, i) => { const isActive = safeActiveTab === i - const isAnswered = (selections[i]?.length ?? 0) > 0 + const isAnswered = (freeTexts[i]?.trim()?.length ?? 0) > 0 || (selections[i]?.length ?? 0) > 0 const tabLabel = q.header || `Q${i + 1}` return (
) } diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 94af3ba3a..52cd13534 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -167,7 +167,10 @@ export function TabBar() { setContextMenu(null) const otherTabs = tabs.filter((t) => t.sessionId !== sessionId) for (const tab of otherTabs) { - if (isSessionTab(tab)) disconnectSession(tab.sessionId) + if (isSessionTab(tab)) { + const state = useChatStore.getState().sessions[tab.sessionId] + if (!state || state.chatState === 'idle') disconnectSession(tab.sessionId) + } closeTabWithCleanup(tab) } } @@ -177,7 +180,10 @@ export function TabBar() { const idx = tabs.findIndex((t) => t.sessionId === sessionId) const leftTabs = tabs.slice(0, idx) for (const tab of leftTabs) { - if (isSessionTab(tab)) disconnectSession(tab.sessionId) + if (isSessionTab(tab)) { + const state = useChatStore.getState().sessions[tab.sessionId] + if (!state || state.chatState === 'idle') disconnectSession(tab.sessionId) + } closeTabWithCleanup(tab) } } @@ -187,7 +193,10 @@ export function TabBar() { const idx = tabs.findIndex((t) => t.sessionId === sessionId) const rightTabs = tabs.slice(idx + 1) for (const tab of rightTabs) { - if (isSessionTab(tab)) disconnectSession(tab.sessionId) + if (isSessionTab(tab)) { + const state = useChatStore.getState().sessions[tab.sessionId] + if (!state || state.chatState === 'idle') disconnectSession(tab.sessionId) + } closeTabWithCleanup(tab) } } @@ -195,7 +204,10 @@ export function TabBar() { const handleCloseAll = () => { setContextMenu(null) for (const tab of tabs) { - if (isSessionTab(tab)) disconnectSession(tab.sessionId) + if (isSessionTab(tab)) { + const state = useChatStore.getState().sessions[tab.sessionId] + if (!state || state.chatState === 'idle') disconnectSession(tab.sessionId) + } closeTabWithCleanup(tab) } } diff --git a/desktop/src/components/workspace/WorkspacePanel.tsx b/desktop/src/components/workspace/WorkspacePanel.tsx index 1adfa2164..705d04c07 100644 --- a/desktop/src/components/workspace/WorkspacePanel.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.tsx @@ -321,15 +321,31 @@ function getLineRangeForText(value: string, text: string) { function FloatingSelectionMenu({ selection, onAdd, + onDismiss, }: { selection: FloatingSelectionMenuState | null onAdd: () => void + onDismiss: () => void }) { const t = useTranslation() + const ref = useRef(null) + + useEffect(() => { + if (!selection) return + const handle = (event: globalThis.MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + onDismiss() + } + } + document.addEventListener('mousedown', handle) + return () => document.removeEventListener('mousedown', handle) + }, [selection, onDismiss]) + if (!selection) return null return (