From 2cd1200081d7e4b70022adc139f2da7076f772ad Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:27:55 +0300 Subject: [PATCH 01/22] fix(shell): hide prompt options on desktop --- src/components/shell/view/Shell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shell/view/Shell.tsx b/src/components/shell/view/Shell.tsx index c133378d2..cbb37547e 100644 --- a/src/components/shell/view/Shell.tsx +++ b/src/components/shell/view/Shell.tsx @@ -310,7 +310,7 @@ export default function Shell({ {cliPromptOptions && isConnected && (
e.preventDefault()} >
From a9e24e7071440f789840b2942e41863de4047a93 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:41:52 +0300 Subject: [PATCH 02/22] fix(chat): group continuous same-tool runs more consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consecutive tool calls (Edit, Read, Grep, etc.) grouped inconsistently: - The group threshold was 3, so a run of only 2 calls stayed ungrouped while a run of 3 collapsed — making two back-to-back edits look different from three. - A run was broken by any interleaved message, including ones that render nothing (reasoning hidden when showThinking is off). Providers like Codex interleave hidden reasoning between tool calls, so visually continuous edits intermittently failed to group. Lower TOOL_GROUP_THRESHOLD to 2 and skip non-rendered messages when extending a run, so any 2+ consecutive same-tool calls collapse reliably. ChatMessagesPane now passes showThinking into groupConsecutiveTools. --- src/components/chat/utils/toolGrouping.ts | 37 ++++++++++++++----- .../view/subcomponents/ChatMessagesPane.tsx | 5 ++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/components/chat/utils/toolGrouping.ts b/src/components/chat/utils/toolGrouping.ts index c9d564330..6a9645714 100644 --- a/src/components/chat/utils/toolGrouping.ts +++ b/src/components/chat/utils/toolGrouping.ts @@ -1,6 +1,6 @@ import type { ChatMessage } from '../types/types'; -export const TOOL_GROUP_THRESHOLD = 3; +export const TOOL_GROUP_THRESHOLD = 2; export interface ToolGroupItem { _isGroup: true; @@ -19,7 +19,17 @@ function isGroupableToolMessage(message: ChatMessage): message is ChatMessage & return Boolean(message.isToolUse && message.toolName && !message.isSubagentContainer); } -export function groupConsecutiveTools(messages: ChatMessage[]): MessageListItem[] { +// Messages that render nothing (e.g. reasoning hidden when showThinking is off) +// shouldn't split an otherwise-continuous run of the same tool — providers like +// Codex interleave hidden reasoning between consecutive tool calls. +function rendersNothing(message: ChatMessage, showThinking: boolean): boolean { + return Boolean(message.isThinking && !showThinking); +} + +export function groupConsecutiveTools( + messages: ChatMessage[], + showThinking: boolean = true, +): MessageListItem[] { const items: MessageListItem[] = []; let index = 0; @@ -35,13 +45,22 @@ export function groupConsecutiveTools(messages: ChatMessage[]): MessageListItem[ const run: ChatMessage[] = [message]; let nextIndex = index + 1; - while ( - nextIndex < messages.length && - isGroupableToolMessage(messages[nextIndex]) && - messages[nextIndex].toolName === message.toolName - ) { - run.push(messages[nextIndex]); - nextIndex += 1; + while (nextIndex < messages.length) { + const candidate = messages[nextIndex]; + + // Skip invisible interleaved messages so they don't break the run. + if (rendersNothing(candidate, showThinking)) { + nextIndex += 1; + continue; + } + + if (isGroupableToolMessage(candidate) && candidate.toolName === message.toolName) { + run.push(candidate); + nextIndex += 1; + continue; + } + + break; } if (run.length >= TOOL_GROUP_THRESHOLD) { diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index bb61096a6..55029c58f 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -120,7 +120,10 @@ function ChatMessagesPane({ const messageKeyMapRef = useRef>(new WeakMap()); const allocatedKeysRef = useRef>(new Set()); const generatedMessageKeyCounterRef = useRef(0); - const groupedVisibleMessages = useMemo(() => groupConsecutiveTools(visibleMessages), [visibleMessages]); + const groupedVisibleMessages = useMemo( + () => groupConsecutiveTools(visibleMessages, Boolean(showThinking)), + [visibleMessages, showThinking], + ); // Keep keys stable across prepends so existing MessageComponent instances retain local state. const getMessageKey = useCallback((message: ChatMessage) => { From 4c6e9178f60c77298860987ef54c6dac3412b20e Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:37:50 +0300 Subject: [PATCH 03/22] fix(chat): stabilize message scroll controls --- .../chat/hooks/useChatSessionState.ts | 54 ++++++++++------- src/components/chat/view/ChatInterface.tsx | 21 +++++-- .../chat/view/subcomponents/ChatComposer.tsx | 20 +------ .../view/subcomponents/ChatMessagesPane.tsx | 58 +++++++++++-------- 4 files changed, 85 insertions(+), 68 deletions(-) diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index 780e5ec37..47f668309 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -121,6 +121,7 @@ export function useChatSessionState({ const [viewHiddenCount, setViewHiddenCount] = useState(0); const scrollContainerRef = useRef(null); + const wasNearTopRef = useRef(false); const [searchTarget, setSearchTarget] = useState<{ timestamp?: string; uuid?: string; snippet?: string } | null>(null); const searchScrollActiveRef = useRef(false); const isLoadingSessionRef = useRef(false); @@ -185,6 +186,7 @@ export function useChatSessionState({ setShowLoadAllOverlay(false); setViewHiddenCount(0); setSearchTarget(null); + wasNearTopRef.current = false; searchScrollActiveRef.current = false; topLoadLockRef.current = false; pendingScrollRestoreRef.current = null; @@ -357,8 +359,25 @@ export function useChatSessionState({ const nearBottom = isNearBottom(); setIsUserScrolledUp(!nearBottom); + const scrolledNearTop = container.scrollTop < 100; + + // "Load all" prompt: appear (with fade-in) when the user reaches the top + if (scrolledNearTop && hasMoreMessages && !allMessagesLoadedRef.current) { + if (!wasNearTopRef.current) { + wasNearTopRef.current = true; + if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current); + + setShowLoadAllOverlay(true); + loadAllOverlayTimerRef.current = setTimeout(() => { + setShowLoadAllOverlay(false); + loadAllOverlayTimerRef.current = null; + }, 5000); + } + } else if (!scrolledNearTop) { + wasNearTopRef.current = false; + } + if (!allMessagesLoadedRef.current) { - const scrolledNearTop = container.scrollTop < 100; if (!scrolledNearTop) { topLoadLockRef.current = false; return; } if (topLoadLockRef.current) { if (container.scrollTop > 20) topLoadLockRef.current = false; @@ -367,7 +386,7 @@ export function useChatSessionState({ const didLoad = await loadOlderMessages(container); if (didLoad) topLoadLockRef.current = true; } - }, [isNearBottom, loadOlderMessages]); + }, [hasMoreMessages, isNearBottom, loadOlderMessages]); useLayoutEffect(() => { if (!pendingScrollRestoreRef.current || !scrollContainerRef.current) return; @@ -386,6 +405,7 @@ export function useChatSessionState({ } topLoadLockRef.current = false; pendingScrollRestoreRef.current = null; + wasNearTopRef.current = false; setIsUserScrolledUp(false); }, [selectedProject?.projectId, selectedSession?.id]); @@ -492,6 +512,7 @@ export function useChatSessionState({ setLoadAllJustFinished(false); setShowLoadAllOverlay(false); setViewHiddenCount(0); + wasNearTopRef.current = false; if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current); if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current); @@ -720,23 +741,8 @@ export function useChatSessionState({ return () => container.removeEventListener('scroll', handleScroll); }, [handleScroll]); - // "Load all" overlay - const prevLoadingRef = useRef(false); - useEffect(() => { - const wasLoading = prevLoadingRef.current; - prevLoadingRef.current = isLoadingMoreMessages; - - if (wasLoading && !isLoadingMoreMessages && hasMoreMessages) { - if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current); - setShowLoadAllOverlay(true); - loadAllOverlayTimerRef.current = setTimeout(() => setShowLoadAllOverlay(false), 2000); - } - if (!hasMoreMessages && !isLoadingMoreMessages) { - if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current); - setShowLoadAllOverlay(false); - } - return () => { if (loadAllOverlayTimerRef.current) clearTimeout(loadAllOverlayTimerRef.current); }; - }, [isLoadingMoreMessages, hasMoreMessages]); + // "Load all" overlay visibility is driven by scroll-to-top in handleScroll; + // timers are cleared on session change via the reset effect above. const loadAllMessages = useCallback(async () => { if (!selectedSession || !selectedProject) return; @@ -746,6 +752,10 @@ export function useChatSessionState({ isLoadingMoreRef.current = true; setIsLoadingAllMessages(true); setShowLoadAllOverlay(true); + if (loadAllOverlayTimerRef.current) { + clearTimeout(loadAllOverlayTimerRef.current); + loadAllOverlayTimerRef.current = null; + } const container = scrollContainerRef.current; const previousScrollHeight = container ? container.scrollHeight : 0; @@ -772,7 +782,11 @@ export function useChatSessionState({ setLoadAllJustFinished(true); if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current); - loadAllFinishedTimerRef.current = setTimeout(() => { setLoadAllJustFinished(false); setShowLoadAllOverlay(false); }, 1000); + loadAllFinishedTimerRef.current = setTimeout(() => { + setLoadAllJustFinished(false); + setShowLoadAllOverlay(false); + loadAllFinishedTimerRef.current = null; + }, 1000); } else { allMessagesLoadedRef.current = false; setShowLoadAllOverlay(false); diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index b466c6ba6..87811c65f 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { ArrowDownIcon } from 'lucide-react'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext'; import { useWebSocket } from '../../../contexts/WebSocketContext'; @@ -362,7 +363,21 @@ function ChatInterface({ selectedProject={selectedProject} /> - + {isUserScrolledUp && chatMessages.length > 0 && ( +
+ +
+ )} + + 0} - onScrollToBottom={scrollToBottomAndReset} onSubmit={handleSubmit} isDragActive={isDragActive} attachedImages={attachedImages} @@ -430,6 +442,7 @@ function ChatInterface({ isTextareaExpanded={isTextareaExpanded} sendByCtrlEnter={sendByCtrlEnter} /> +
diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx index 6077ca2bf..e7b36c602 100644 --- a/src/components/chat/view/subcomponents/ChatComposer.tsx +++ b/src/components/chat/view/subcomponents/ChatComposer.tsx @@ -11,7 +11,7 @@ import type { RefObject, TouchEvent, } from 'react'; -import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon, Loader2 } from 'lucide-react'; +import { ImageIcon, MessageSquareIcon, XIcon, Loader2 } from 'lucide-react'; import { useVoiceInput } from '../../hooks/useVoiceInput'; import { useVoiceAvailable } from '../../hooks/useVoiceAvailable'; @@ -68,9 +68,6 @@ interface ChatComposerProps { onToggleCommandMenu: () => void; hasInput: boolean; onClearInput: () => void; - isUserScrolledUp: boolean; - hasMessages: boolean; - onScrollToBottom: () => void; onSubmit: (event: FormEvent | MouseEvent | TouchEvent) => void; isDragActive: boolean; attachedImages: File[]; @@ -122,9 +119,6 @@ export default function ChatComposer({ onToggleCommandMenu, hasInput, onClearInput, - isUserScrolledUp, - hasMessages, - onScrollToBottom, onSubmit, isDragActive, attachedImages, @@ -219,18 +213,6 @@ export default function ChatComposer({ )} {!hasQuestionPanel &&
- {isUserScrolledUp && hasMessages && ( -
- -
- )} {showFileDropdown && filteredFiles.length > 0 && (
{filteredFiles.map((file, index) => ( diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index 55029c58f..78d212b9c 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { memo, useCallback, useMemo, useRef } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import type { Dispatch, RefObject, SetStateAction } from 'react'; import type { ChatMessage } from '../../types/types'; @@ -117,37 +117,45 @@ function ChatMessagesPane({ selectedProject, }: ChatMessagesPaneProps) { const { t } = useTranslation('chat'); - const messageKeyMapRef = useRef>(new WeakMap()); - const allocatedKeysRef = useRef>(new Set()); - const generatedMessageKeyCounterRef = useRef(0); const groupedVisibleMessages = useMemo( () => groupConsecutiveTools(visibleMessages, Boolean(showThinking)), [visibleMessages, showThinking], ); - // Keep keys stable across prepends so existing MessageComponent instances retain local state. - const getMessageKey = useCallback((message: ChatMessage) => { - const existingKey = messageKeyMapRef.current.get(message); - if (existingKey) { - return existingKey; + // Stable, deterministic keys for the messages rendered this pass. + // + // `normalizedToChatMessages` rebuilds fresh ChatMessage objects on every store + // update, so caching keys by object identity (or via a cross-render allocation + // Set) minted a brand-new key for the *same* logical message on each prepend — + // remounting the whole list, which disconnects the scroll-restore anchor and + // reflows heights, jumping the viewport to the bottom. Deriving keys purely + // from this render's ordered messages (intrinsic key, disambiguated by + // occurrence index on collision) yields the same key for the same message + // order, so React preserves existing DOM nodes and component state on prepend. + const messageKeyMap = useMemo(() => { + const keys = new WeakMap(); + const occurrences = new Map(); + const assign = (message: ChatMessage) => { + const intrinsicKey = getIntrinsicMessageKey(message) ?? 'message-generated'; + const seen = occurrences.get(intrinsicKey) ?? 0; + occurrences.set(intrinsicKey, seen + 1); + keys.set(message, seen === 0 ? intrinsicKey : `${intrinsicKey}__${seen}`); + }; + for (const item of groupedVisibleMessages) { + if (isToolGroupItem(item)) { + item.messages.forEach(assign); + } else { + assign(item); + } } + return keys; + }, [groupedVisibleMessages]); - const intrinsicKey = getIntrinsicMessageKey(message); - let candidateKey = intrinsicKey; - - if (!candidateKey || allocatedKeysRef.current.has(candidateKey)) { - do { - generatedMessageKeyCounterRef.current += 1; - candidateKey = intrinsicKey - ? `${intrinsicKey}-${generatedMessageKeyCounterRef.current}` - : `message-generated-${generatedMessageKeyCounterRef.current}`; - } while (allocatedKeysRef.current.has(candidateKey)); - } - - allocatedKeysRef.current.add(candidateKey); - messageKeyMapRef.current.set(message, candidateKey); - return candidateKey; - }, []); + const getMessageKey = useCallback( + (message: ChatMessage) => + messageKeyMap.get(message) ?? getIntrinsicMessageKey(message) ?? 'message-generated', + [messageKeyMap], + ); return (
Date: Mon, 29 Jun 2026 21:38:39 +0300 Subject: [PATCH 04/22] fix: update command menu positioning --- .../chat/view/subcomponents/CommandMenu.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/chat/view/subcomponents/CommandMenu.tsx b/src/components/chat/view/subcomponents/CommandMenu.tsx index 3e6116a0e..580ec92cf 100644 --- a/src/components/chat/view/subcomponents/CommandMenu.tsx +++ b/src/components/chat/view/subcomponents/CommandMenu.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from 'react'; -import type { CSSProperties } from 'react'; +import { createPortal } from 'react-dom'; +import type { CSSProperties, ReactElement } from 'react'; import { CornerDownLeft, Folder, @@ -77,6 +78,7 @@ const namespaceAccentClasses: Record = { const MENU_EDGE_GAP = 16; const MENU_MAX_HEIGHT = 360; +const MENU_MIN_HEIGHT = 160; const getCommandKey = (command: CommandMenuCommand) => `${command.name}::${command.namespace || command.type || 'other'}::${command.path || ''}`; @@ -92,8 +94,9 @@ const getMenuPosition = (position: { top: number; left: number; bottom?: number if (typeof window === 'undefined') { return { position: 'fixed', top: '16px', left: '16px' }; } + const maxAnchorBottom = Math.max(MENU_EDGE_GAP, window.innerHeight - MENU_EDGE_GAP - MENU_MIN_HEIGHT); if (window.innerWidth < 640) { - const anchorBottom = Math.max(MENU_EDGE_GAP, position.bottom ?? 90); + const anchorBottom = Math.min(Math.max(MENU_EDGE_GAP, position.bottom ?? 90), maxAnchorBottom); return { position: 'fixed', bottom: `${anchorBottom}px`, @@ -104,7 +107,7 @@ const getMenuPosition = (position: { top: number; left: number; bottom?: number maxHeight: `min(54vh, calc(100vh - ${anchorBottom}px - ${MENU_EDGE_GAP}px))`, }; } - const anchorBottom = Math.max(MENU_EDGE_GAP, position.bottom ?? 90); + const anchorBottom = Math.min(Math.max(MENU_EDGE_GAP, position.bottom ?? 90), maxAnchorBottom); const clampedLeft = Math.max( MENU_EDGE_GAP, Math.min(position.left, window.innerWidth - 440 - MENU_EDGE_GAP), @@ -216,9 +219,11 @@ export default function CommandMenu({ : ['builtin', 'skill', 'project', 'user', 'other']; const extraNamespaces = Object.keys(groupedCommands).filter((namespace) => !preferredOrder.includes(namespace)); const orderedNamespaces = [...preferredOrder, ...extraNamespaces].filter((namespace) => groupedCommands[namespace]); + const renderInPortal = (node: ReactElement) => + typeof document === 'undefined' ? node : createPortal(node, document.body); if (commands.length === 0) { - return ( + return renderInPortal(
Date: Mon, 29 Jun 2026 22:22:46 +0300 Subject: [PATCH 05/22] fix(chat): refine load all overlay behavior --- .../chat/hooks/useChatSessionState.ts | 30 +++++++- .../view/subcomponents/ChatMessagesPane.tsx | 37 +++------- .../subcomponents/LoadAllMessagesOverlay.tsx | 68 +++++++++++++++++++ 3 files changed, 103 insertions(+), 32 deletions(-) create mode 100644 src/components/chat/view/subcomponents/LoadAllMessagesOverlay.tsx diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index 47f668309..3330e21b4 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -338,12 +338,36 @@ export function useChatSessionState({ const slot = await sessionStore.fetchMore(selectedSession.id, { limit: MESSAGES_PER_PAGE, }); - if (!slot || slot.serverMessages.length === 0) return false; + if (!slot) return false; + if (slot.serverMessages.length === 0) { + if (!slot.hasMore) { + setHasMoreMessages(false); + allMessagesLoadedRef.current = true; + setAllMessagesLoaded(true); + if (!loadAllOverlayTimerRef.current) { + loadAllOverlayTimerRef.current = setTimeout(() => { + setShowLoadAllOverlay(false); + loadAllOverlayTimerRef.current = null; + }, 2500); + } + } + return false; + } pendingScrollRestoreRef.current = { height: previousScrollHeight, top: previousScrollTop }; setHasMoreMessages(slot.hasMore); setTotalMessages(slot.total); setVisibleMessageCount((prev) => prev + MESSAGES_PER_PAGE); + if (!slot.hasMore) { + allMessagesLoadedRef.current = true; + setAllMessagesLoaded(true); + if (!loadAllOverlayTimerRef.current) { + loadAllOverlayTimerRef.current = setTimeout(() => { + setShowLoadAllOverlay(false); + loadAllOverlayTimerRef.current = null; + }, 2500); + } + } return true; } finally { isLoadingMoreRef.current = false; @@ -371,7 +395,7 @@ export function useChatSessionState({ loadAllOverlayTimerRef.current = setTimeout(() => { setShowLoadAllOverlay(false); loadAllOverlayTimerRef.current = null; - }, 5000); + }, 2500); } } else if (!scrolledNearTop) { wasNearTopRef.current = false; @@ -786,7 +810,7 @@ export function useChatSessionState({ setLoadAllJustFinished(false); setShowLoadAllOverlay(false); loadAllFinishedTimerRef.current = null; - }, 1000); + }, 2500); } else { allMessagesLoadedRef.current = false; setShowLoadAllOverlay(false); diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index 78d212b9c..55be2a094 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -15,6 +15,7 @@ import { groupConsecutiveTools, isToolGroupItem } from '../../utils/toolGrouping import MessageComponent from './MessageComponent'; import ProviderSelectionEmptyState from './ProviderSelectionEmptyState'; import ToolGroupContainer from './ToolGroupContainer'; +import LoadAllMessagesOverlay from './LoadAllMessagesOverlay'; interface ChatMessagesPaneProps { scrollContainerRef: RefObject; @@ -219,35 +220,13 @@ function ChatMessagesPane({
)} - {/* Floating "Load all messages" overlay */} - {(showLoadAllOverlay || isLoadingAllMessages || loadAllJustFinished) && ( -
- {loadAllJustFinished ? ( -
- - - - {t('session.messages.allLoaded')} -
- ) : ( - - )} -
- )} + {/* Legacy message count indicator (for non-paginated view) */} {!hasMoreMessages && chatMessages.length > visibleMessageCount && ( diff --git a/src/components/chat/view/subcomponents/LoadAllMessagesOverlay.tsx b/src/components/chat/view/subcomponents/LoadAllMessagesOverlay.tsx new file mode 100644 index 000000000..ef246756d --- /dev/null +++ b/src/components/chat/view/subcomponents/LoadAllMessagesOverlay.tsx @@ -0,0 +1,68 @@ +import { useTranslation } from 'react-i18next'; + +const loadAllOverlayAnimationStyle = ` +@keyframes loadAllOverlayAutoFade { + 0%, 80% { opacity: 1; } + 100% { opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .load-all-overlay-auto-fade { + animation: none !important; + } +} +`; + +interface LoadAllMessagesOverlayProps { + showLoadAllOverlay: boolean; + isLoadingAllMessages: boolean; + loadAllJustFinished: boolean; + totalMessages: number; + onLoadAllMessages: () => void; +} + +export default function LoadAllMessagesOverlay({ + showLoadAllOverlay, + isLoadingAllMessages, + loadAllJustFinished, + totalMessages, + onLoadAllMessages, +}: LoadAllMessagesOverlayProps) { + const { t } = useTranslation('chat'); + + if (!showLoadAllOverlay && !isLoadingAllMessages && !loadAllJustFinished) { + return null; + } + + return ( +
+ + {loadAllJustFinished ? ( +
+ + + + {t('session.messages.allLoaded')} +
+ ) : ( + + )} +
+ ); +} From 37ef8919455cde79e0ba3fe6d683a8fa845aecbb Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:27:59 +0300 Subject: [PATCH 06/22] fix(chat): hide load all prompt after final page --- .../chat/hooks/useChatSessionState.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index 3330e21b4..9103e28a5 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -344,12 +344,11 @@ export function useChatSessionState({ setHasMoreMessages(false); allMessagesLoadedRef.current = true; setAllMessagesLoaded(true); - if (!loadAllOverlayTimerRef.current) { - loadAllOverlayTimerRef.current = setTimeout(() => { - setShowLoadAllOverlay(false); - loadAllOverlayTimerRef.current = null; - }, 2500); + if (loadAllOverlayTimerRef.current) { + clearTimeout(loadAllOverlayTimerRef.current); + loadAllOverlayTimerRef.current = null; } + setShowLoadAllOverlay(false); } return false; } @@ -361,12 +360,11 @@ export function useChatSessionState({ if (!slot.hasMore) { allMessagesLoadedRef.current = true; setAllMessagesLoaded(true); - if (!loadAllOverlayTimerRef.current) { - loadAllOverlayTimerRef.current = setTimeout(() => { - setShowLoadAllOverlay(false); - loadAllOverlayTimerRef.current = null; - }, 2500); + if (loadAllOverlayTimerRef.current) { + clearTimeout(loadAllOverlayTimerRef.current); + loadAllOverlayTimerRef.current = null; } + setShowLoadAllOverlay(false); } return true; } finally { From 19b59e701effa5ed9ca46e7bec26a796a9735a4c Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:38:16 +0300 Subject: [PATCH 07/22] fix(chat): remove auto scroll quick setting --- .../chat/hooks/useChatSessionState.ts | 18 +++++++----------- src/components/chat/types/types.ts | 1 - src/components/chat/view/ChatInterface.tsx | 2 -- .../main-content/view/MainContent.tsx | 3 +-- .../quick-settings-panel/constants.ts | 10 +--------- src/components/quick-settings-panel/types.ts | 1 - .../view/QuickSettingsContent.tsx | 7 ++----- .../view/QuickSettingsPanelView.tsx | 4 ++-- src/hooks/useUiPreferences.ts | 2 -- src/i18n/locales/de/settings.json | 2 -- src/i18n/locales/en/settings.json | 2 -- src/i18n/locales/fr/settings.json | 2 -- src/i18n/locales/it/settings.json | 2 -- src/i18n/locales/ja/settings.json | 2 -- src/i18n/locales/ko/settings.json | 2 -- src/i18n/locales/ru/settings.json | 2 -- src/i18n/locales/tr/settings.json | 2 -- src/i18n/locales/zh-CN/settings.json | 2 -- src/i18n/locales/zh-TW/settings.json | 2 -- 19 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index 9103e28a5..37540895c 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -18,7 +18,6 @@ interface UseChatSessionStateArgs { selectedSession: ProjectSession | null; ws: WebSocket | null; sendMessage: (message: unknown) => void; - autoScrollToBottom?: boolean; externalMessageUpdate?: number; newSessionTrigger?: number; processingSessions?: SessionActivityMap; @@ -96,7 +95,6 @@ export function useChatSessionState({ selectedSession, ws, sendMessage, - autoScrollToBottom, externalMessageUpdate, newSessionTrigger, processingSessions, @@ -589,7 +587,7 @@ export function useChatSessionState({ if (!isProcessing) { await sessionStore.refreshFromServer(selectedSession.id); - if (Boolean(autoScrollToBottom) && isNearBottom()) { + if (isNearBottom()) { setTimeout(() => scrollToBottom(), 200); } } @@ -600,7 +598,6 @@ export function useChatSessionState({ reloadExternalMessages(); }, [ - autoScrollToBottom, externalMessageUpdate, isNearBottom, scrollToBottom, @@ -732,10 +729,9 @@ export function useChatSessionState({ }, [chatMessages, visibleMessageCount]); useEffect(() => { - if (!autoScrollToBottom && scrollContainerRef.current) { - const container = scrollContainerRef.current; - scrollPositionRef.current = { height: container.scrollHeight, top: container.scrollTop }; - } + const container = scrollContainerRef.current; + if (!container) return; + scrollPositionRef.current = { height: container.scrollHeight, top: container.scrollTop }; }); useEffect(() => { @@ -743,8 +739,8 @@ export function useChatSessionState({ if (isLoadingMoreRef.current || isLoadingMoreMessages || pendingScrollRestoreRef.current) return; if (searchScrollActiveRef.current) return; - if (autoScrollToBottom) { - if (!isUserScrolledUp) setTimeout(() => scrollToBottom(), 50); + if (!isUserScrolledUp) { + setTimeout(() => scrollToBottom(), 50); return; } @@ -754,7 +750,7 @@ export function useChatSessionState({ const newHeight = container.scrollHeight; const heightDiff = newHeight - prevHeight; if (heightDiff > 0 && prevTop > 0) container.scrollTop = prevTop + heightDiff; - }, [autoScrollToBottom, chatMessages.length, isLoadingMoreMessages, isUserScrolledUp, scrollToBottom]); + }, [chatMessages.length, isLoadingMoreMessages, isUserScrolledUp, scrollToBottom]); useEffect(() => { const container = scrollContainerRef.current; diff --git a/src/components/chat/types/types.ts b/src/components/chat/types/types.ts index 60ee18fdd..fdeab8df5 100644 --- a/src/components/chat/types/types.ts +++ b/src/components/chat/types/types.ts @@ -129,7 +129,6 @@ export interface ChatInterfaceProps { autoExpandTools?: boolean; showRawParameters?: boolean; showThinking?: boolean; - autoScrollToBottom?: boolean; sendByCtrlEnter?: boolean; externalMessageUpdate?: number; newSessionTrigger?: number; diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 87811c65f..c4a3d202b 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -34,7 +34,6 @@ function ChatInterface({ autoExpandTools, showRawParameters, showThinking, - autoScrollToBottom, sendByCtrlEnter, externalMessageUpdate, newSessionTrigger, @@ -125,7 +124,6 @@ function ChatInterface({ selectedSession, ws, sendMessage, - autoScrollToBottom, externalMessageUpdate, newSessionTrigger, processingSessions, diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index 96877ac00..9b00f926f 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -54,7 +54,7 @@ function MainContent({ newSessionTrigger, }: MainContentProps) { const { preferences } = useUiPreferences(); - const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences; + const { autoExpandTools, showRawParameters, showThinking, sendByCtrlEnter } = preferences; const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue; const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue; @@ -173,7 +173,6 @@ function MainContent({ autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} showThinking={showThinking} - autoScrollToBottom={autoScrollToBottom} sendByCtrlEnter={sendByCtrlEnter} externalMessageUpdate={externalMessageUpdate} newSessionTrigger={newSessionTrigger} diff --git a/src/components/quick-settings-panel/constants.ts b/src/components/quick-settings-panel/constants.ts index 408a64c7b..9c5fd5c39 100644 --- a/src/components/quick-settings-panel/constants.ts +++ b/src/components/quick-settings-panel/constants.ts @@ -1,11 +1,11 @@ import { - ArrowDown, Brain, Eye, Languages, Maximize2, Mic, } from 'lucide-react'; + import type { PreferenceToggleItem } from './types'; export const HANDLE_POSITION_STORAGE_KEY = 'quickSettingsHandlePosition'; @@ -41,14 +41,6 @@ export const TOOL_DISPLAY_TOGGLES: PreferenceToggleItem[] = [ }, ]; -export const VIEW_OPTION_TOGGLES: PreferenceToggleItem[] = [ - { - key: 'autoScrollToBottom', - labelKey: 'quickSettings.autoScrollToBottom', - icon: ArrowDown, - }, -]; - export const INPUT_SETTING_TOGGLES: PreferenceToggleItem[] = [ { key: 'sendByCtrlEnter', diff --git a/src/components/quick-settings-panel/types.ts b/src/components/quick-settings-panel/types.ts index 8d4f08265..a02401e8f 100644 --- a/src/components/quick-settings-panel/types.ts +++ b/src/components/quick-settings-panel/types.ts @@ -5,7 +5,6 @@ export type PreferenceToggleKey = | 'autoExpandTools' | 'showRawParameters' | 'showThinking' - | 'autoScrollToBottom' | 'sendByCtrlEnter' | 'voiceEnabled'; diff --git a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx index dc5396218..b8bd1032d 100644 --- a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx +++ b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx @@ -1,18 +1,19 @@ import { Moon, Sun } from 'lucide-react'; import { useTranslation } from 'react-i18next'; + import { DarkModeToggle } from '../../../shared/view/ui'; import LanguageSelector from '../../../shared/view/ui/LanguageSelector'; import { INPUT_SETTING_TOGGLES, SETTING_ROW_CLASS, TOOL_DISPLAY_TOGGLES, - VIEW_OPTION_TOGGLES, } from '../constants'; import type { PreferenceToggleItem, PreferenceToggleKey, QuickSettingsPreferences, } from '../types'; + import QuickSettingsSection from './QuickSettingsSection'; import QuickSettingsToggleRow from './QuickSettingsToggleRow'; @@ -65,10 +66,6 @@ export default function QuickSettingsContent({ {renderToggleRows(TOOL_DISPLAY_TOGGLES)} - - {renderToggleRows(VIEW_OPTION_TOGGLES)} - - {renderToggleRows(inputSettingToggles)}

diff --git a/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx b/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx index 5f630a610..d42a73062 100644 --- a/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx +++ b/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx @@ -1,10 +1,12 @@ import { useCallback, useMemo, useState } from 'react'; import type { MouseEvent as ReactMouseEvent } from 'react'; + import { useDeviceSettings } from '../../../hooks/useDeviceSettings'; import { useUiPreferences } from '../../../hooks/useUiPreferences'; import { useTheme } from '../../../contexts/ThemeContext'; import { useQuickSettingsDrag } from '../hooks/useQuickSettingsDrag'; import type { PreferenceToggleKey, QuickSettingsPreferences } from '../types'; + import QuickSettingsContent from './QuickSettingsContent'; import QuickSettingsHandle from './QuickSettingsHandle'; import QuickSettingsPanelHeader from './QuickSettingsPanelHeader'; @@ -25,12 +27,10 @@ export default function QuickSettingsPanelView() { autoExpandTools: preferences.autoExpandTools, showRawParameters: preferences.showRawParameters, showThinking: preferences.showThinking, - autoScrollToBottom: preferences.autoScrollToBottom, sendByCtrlEnter: preferences.sendByCtrlEnter, voiceEnabled: preferences.voiceEnabled, }), [ preferences.autoExpandTools, - preferences.autoScrollToBottom, preferences.sendByCtrlEnter, preferences.showRawParameters, preferences.showThinking, diff --git a/src/hooks/useUiPreferences.ts b/src/hooks/useUiPreferences.ts index 342f16986..b4531ba96 100644 --- a/src/hooks/useUiPreferences.ts +++ b/src/hooks/useUiPreferences.ts @@ -4,7 +4,6 @@ type UiPreferences = { autoExpandTools: boolean; showRawParameters: boolean; showThinking: boolean; - autoScrollToBottom: boolean; sendByCtrlEnter: boolean; sidebarVisible: boolean; voiceEnabled: boolean; @@ -37,7 +36,6 @@ const DEFAULTS: UiPreferences = { autoExpandTools: false, showRawParameters: false, showThinking: true, - autoScrollToBottom: true, sendByCtrlEnter: false, sidebarVisible: true, voiceEnabled: false, diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 237e59507..85fb4c845 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "Darstellung", "toolDisplay": "Werkzeuganzeige", - "viewOptions": "Anzeigeoptionen", "inputSettings": "Eingabeeinstellungen" }, "darkMode": "Darkmode", "autoExpandTools": "Werkzeuge automatisch erweitern", "showRawParameters": "Rohe Parameter anzeigen", "showThinking": "Denken anzeigen", - "autoScrollToBottom": "Automatisch nach unten scrollen", "sendByCtrlEnter": "Mit Strg+Enter senden", "sendByCtrlEnterDescription": "Wenn aktiviert, sendet Strg+Enter die Nachricht anstelle von Enter. Dies ist nützlich für IME-Benutzer:innen, um versehentliches Senden zu vermeiden.", "dragHandle": { diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 1622b916b..7c779d219 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -70,14 +70,12 @@ "sections": { "appearance": "Appearance", "toolDisplay": "Tool Display", - "viewOptions": "View Options", "inputSettings": "Input Settings" }, "darkMode": "Dark Mode", "autoExpandTools": "Auto-expand tools", "showRawParameters": "Show raw parameters", "showThinking": "Show thinking", - "autoScrollToBottom": "Auto-scroll to bottom", "sendByCtrlEnter": "Send by Ctrl+Enter", "voiceEnabled": "Voice (mic + read aloud)", "sendByCtrlEnterDescription": "When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends.", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 4fab7e1e8..ec3c59ec7 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "Apparence", "toolDisplay": "Affichage des outils", - "viewOptions": "Options d'affichage", "inputSettings": "Paramètres de saisie" }, "darkMode": "Mode sombre", "autoExpandTools": "Développer automatiquement les outils", "showRawParameters": "Afficher les paramètres bruts", "showThinking": "Afficher la réflexion", - "autoScrollToBottom": "Défilement automatique vers le bas", "sendByCtrlEnter": "Envoyer avec Ctrl+Entrée", "sendByCtrlEnterDescription": "Lorsqu'activé, appuyer sur Ctrl+Entrée envoie le message au lieu de simplement Entrée. Utile pour les utilisateurs IME pour éviter les envois accidentels.", "dragHandle": { diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index d283bdb92..28e8a1e30 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "Aspetto", "toolDisplay": "Visualizzazione strumenti", - "viewOptions": "Opzioni visualizzazione", "inputSettings": "Impostazioni input" }, "darkMode": "Modalità scura", "autoExpandTools": "Espandi strumenti automaticamente", "showRawParameters": "Mostra parametri grezzi", "showThinking": "Mostra ragionamento", - "autoScrollToBottom": "Scorrimento automatico in basso", "sendByCtrlEnter": "Invia con Ctrl+Invio", "sendByCtrlEnterDescription": "Se abilitato, premere Ctrl+Invio invierà il messaggio invece di Invio. Utile per gli utenti IME per evitare invii accidentali.", "dragHandle": { diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index 0ad10c46f..d59e32a03 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "外観", "toolDisplay": "ツール表示", - "viewOptions": "表示オプション", "inputSettings": "入力設定" }, "darkMode": "ダークモード", "autoExpandTools": "ツールを自動展開", "showRawParameters": "生パラメータを表示", "showThinking": "思考を表示", - "autoScrollToBottom": "自動スクロール", "sendByCtrlEnter": "Ctrl+Enterで送信", "sendByCtrlEnterDescription": "有効にすると、Enterではなく Ctrl+Enter でメッセージを送信します。IMEユーザーの誤送信防止に便利です。", "dragHandle": { diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index 3fd7a2854..c45c7227b 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "외관", "toolDisplay": "도구 표시", - "viewOptions": "보기 옵션", "inputSettings": "입력 설정" }, "darkMode": "다크 모드", "autoExpandTools": "도구 자동 펼치기", "showRawParameters": "Raw 파라미터 표시", "showThinking": "생각 과정 표시", - "autoScrollToBottom": "자동 스크롤", "sendByCtrlEnter": "Ctrl+Enter로 전송", "sendByCtrlEnterDescription": "활성화하면 Enter 대신 Ctrl+Enter로 메시지를 전송합니다. IME 사용자가 실수로 전송하는 것을 방지하는 데 유용합니다.", "dragHandle": { diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 94e88f377..f83881dcb 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "Внешний вид", "toolDisplay": "Отображение инструментов", - "viewOptions": "Параметры просмотра", "inputSettings": "Настройки ввода" }, "darkMode": "Темная тема", "autoExpandTools": "Автоматически разворачивать инструменты", "showRawParameters": "Показывать сырые параметры", "showThinking": "Показывать размышления", - "autoScrollToBottom": "Автопрокрутка вниз", "sendByCtrlEnter": "Отправка по Ctrl+Enter", "sendByCtrlEnterDescription": "Когда включено, нажатие Ctrl+Enter будет отправлять сообщение вместо просто Enter. Это полезно для пользователей IME, чтобы избежать случайной отправки.", "dragHandle": { diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index 7ce306565..4c56722a4 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "Görünüm", "toolDisplay": "Araç Gösterimi", - "viewOptions": "Görünüm Seçenekleri", "inputSettings": "Girdi Ayarları" }, "darkMode": "Koyu Mod", "autoExpandTools": "Araçları otomatik genişlet", "showRawParameters": "Ham parametreleri göster", "showThinking": "Düşünmeyi göster", - "autoScrollToBottom": "Otomatik en alta kaydır", "sendByCtrlEnter": "Ctrl+Enter ile gönder", "sendByCtrlEnterDescription": "Etkinleştirildiğinde, Ctrl+Enter'a basmak yalnız Enter yerine mesajı gönderir. IME (girdi metot düzenleyici) kullananlar için yanlışlıkla göndermeyi önler.", "dragHandle": { diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 529a35cc6..518edb43b 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "外观", "toolDisplay": "工具显示", - "viewOptions": "视图选项", "inputSettings": "输入设置" }, "darkMode": "深色模式", "autoExpandTools": "自动展开工具", "showRawParameters": "显示原始参数", "showThinking": "显示思考过程", - "autoScrollToBottom": "自动滚动到底部", "sendByCtrlEnter": "使用 Ctrl+Enter 发送", "sendByCtrlEnterDescription": "启用后,按 Ctrl+Enter 发送消息,而不是仅按 Enter。这对于使用输入法的用户可以避免意外发送。", "dragHandle": { diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 22c73d6e0..7bda49eef 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -54,14 +54,12 @@ "sections": { "appearance": "外觀", "toolDisplay": "工具顯示", - "viewOptions": "檢視選項", "inputSettings": "輸入設定" }, "darkMode": "深色模式", "autoExpandTools": "自動展開工具", "showRawParameters": "顯示原始參數", "showThinking": "顯示思考過程", - "autoScrollToBottom": "自動捲動到底部", "sendByCtrlEnter": "使用 Ctrl+Enter 傳送", "sendByCtrlEnterDescription": "啟用後,按 Ctrl+Enter 傳送訊息,而不是僅按 Enter。這對於使用輸入法的使用者可以避免意外傳送。", "dragHandle": { From e71f3bf3f6556bc09ae1a1813081532195e29985 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:52:35 +0300 Subject: [PATCH 08/22] fix(chat): unify messages and composer into centered column Constrain both ChatMessagesPane content and ChatComposer to the same max-w-3xl centered column. Previously only the composer had a max-width, causing messages to fill the full width while the input stayed narrow, making them visually misaligned with large empty gutters on either side. --- src/components/chat/view/subcomponents/ChatComposer.tsx | 4 ++-- src/components/chat/view/subcomponents/ChatMessagesPane.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx index e7b36c602..bb1c4a45c 100644 --- a/src/components/chat/view/subcomponents/ChatComposer.tsx +++ b/src/components/chat/view/subcomponents/ChatComposer.tsx @@ -203,7 +203,7 @@ export default function ChatComposer({ )} {pendingPermissionRequests.length > 0 && ( -

+
)} - {!hasQuestionPanel &&
+ {!hasQuestionPanel &&
{showFileDropdown && filteredFiles.length > 0 && (
{filteredFiles.map((file, index) => ( diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index 55be2a094..a437a4f89 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -163,8 +163,9 @@ function ChatMessagesPane({ ref={scrollContainerRef} onWheel={onWheel} onTouchMove={onTouchMove} - className="chat-messages-pane relative min-h-0 flex-1 space-y-3 overflow-y-auto overflow-x-hidden px-0 py-3 sm:space-y-4 sm:p-4" + className="chat-messages-pane relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden py-3 sm:py-4" > +
{(isLoadingSessionMessages || isProcessing) && chatMessages.length === 0 ? (
@@ -295,6 +296,7 @@ function ChatMessagesPane({ })()} )} +
); } From 032258b260ac88ee26057e595f454bda4119db79 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:45:00 +0300 Subject: [PATCH 09/22] style(ui): rework light/dark theme to make it visually consistent Rework the color system around warm neutrals and route hardcoded surfaces through theme tokens for consistency. - Theme tokens (index.css, ThemeContext): warm cream light mode and neutral charcoal dark mode, replacing the pure-white/blue-tinted palette; update PWA theme-color meta - Code blocks: soft grey background in light mode via oneLight/oneDark, and drop the Tailwind Typography
 shell that
  framed the highlighter in a dark box
- Dropdowns/panels: convert CommandMenu, Quick Settings, and the JSON
  response block from hardcoded gray/slate to popover/muted/border
  tokens
- Git panel: Publish button purple -> primary blue
- Composer: drop top padding so the input sits flush with the thread
---
 .../chat/view/subcomponents/ChatComposer.tsx  |  2 +-
 .../chat/view/subcomponents/CommandMenu.tsx   | 22 ++---
 .../chat/view/subcomponents/Markdown.tsx      | 17 +++-
 .../view/subcomponents/MessageComponent.tsx   |  6 +-
 .../markdown/MarkdownCodeBlock.tsx            | 12 ++-
 .../markdown/MarkdownPreview.tsx              |  3 +
 .../git-panel/view/GitPanelHeader.tsx         |  2 +-
 .../quick-settings-panel/constants.ts         |  2 +-
 .../view/QuickSettingsContent.tsx             |  8 +-
 .../view/QuickSettingsPanelHeader.tsx         |  6 +-
 .../view/QuickSettingsSection.tsx             |  2 +-
 .../view/QuickSettingsToggleRow.tsx           |  4 +-
 src/contexts/ThemeContext.jsx                 |  4 +-
 src/index.css                                 | 90 +++++++++----------
 14 files changed, 98 insertions(+), 82 deletions(-)

diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx
index bb1c4a45c..c021d9197 100644
--- a/src/components/chat/view/subcomponents/ChatComposer.tsx
+++ b/src/components/chat/view/subcomponents/ChatComposer.tsx
@@ -197,7 +197,7 @@ export default function ChatComposer({
   const hasPendingPermissions = pendingPermissionRequests.length > 0;
 
   return (
-    
+
{!hasPendingPermissions && ( )} diff --git a/src/components/chat/view/subcomponents/CommandMenu.tsx b/src/components/chat/view/subcomponents/CommandMenu.tsx index 580ec92cf..51b90e09f 100644 --- a/src/components/chat/view/subcomponents/CommandMenu.tsx +++ b/src/components/chat/view/subcomponents/CommandMenu.tsx @@ -226,7 +226,7 @@ export default function CommandMenu({ return renderInPortal(
{orderedNamespaces.map((namespace) => (
{orderedNamespaces.length > 1 && ( -
+
{namespaceLabels[namespace] || namespace} - + {(groupedCommands[namespace] || []).length}
@@ -273,15 +273,15 @@ export default function CommandMenu({ aria-selected={isSelected} className={`command-item group relative mb-1 flex cursor-pointer items-start gap-2 rounded-md border px-2.5 py-2 transition-all ${ isSelected - ? 'border-sky-200 bg-sky-50 shadow-sm dark:border-cyan-400/30 dark:bg-cyan-400/10' - : 'border-transparent bg-transparent hover:border-gray-200 hover:bg-gray-50/90 dark:hover:border-slate-700 dark:hover:bg-slate-900/80' + ? 'border-primary/30 bg-primary/10 shadow-sm' + : 'border-transparent bg-transparent hover:border-border hover:bg-accent' }`} onMouseEnter={() => onSelect && commandIndex >= 0 && onSelect(command, commandIndex, true)} onClick={() => onSelect && commandIndex >= 0 && onSelect(command, commandIndex, false)} onMouseDown={(event) => event.preventDefault()} > {isSelected && ( - + )}