diff --git a/src/web-ui/src/app/scenes/SceneViewport.tsx b/src/web-ui/src/app/scenes/SceneViewport.tsx index 4a42ae82..08aa5aff 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.tsx +++ b/src/web-ui/src/app/scenes/SceneViewport.tsx @@ -16,9 +16,11 @@ import { useDialogCompletionNotify } from '../hooks/useDialogCompletionNotify'; import { ProcessingIndicator } from '@/flow_chat/components/modern/ProcessingIndicator'; import SettingsScene from './settings/SettingsScene'; import AssistantScene from './assistant/AssistantScene'; +import SessionScene from './session/SessionScene'; import './SceneViewport.scss'; -const SessionScene = lazy(() => import('./session/SessionScene')); +// Session is the primary interaction path. Keep it in the main scene bundle so +// first open does not stall on a lazy chunk fetch/parse before FlowChat mounts. const TerminalScene = lazy(() => import('./terminal/TerminalScene')); const GitScene = lazy(() => import('./git/GitScene')); const FileViewerScene = lazy(() => import('./file-viewer/FileViewerScene')); diff --git a/src/web-ui/src/flow_chat/services/openBtwSession.ts b/src/web-ui/src/flow_chat/services/openBtwSession.ts index 211b1a1f..cc319ef8 100644 --- a/src/web-ui/src/flow_chat/services/openBtwSession.ts +++ b/src/web-ui/src/flow_chat/services/openBtwSession.ts @@ -7,6 +7,7 @@ import { useAgentCanvasStore } from '@/app/components/panels/content-canvas/stor import type { CanvasTab } from '@/app/components/panels/content-canvas/types'; import { flowChatStore } from '../store/FlowChatStore'; import { flowChatManager } from './FlowChatManager'; +import { syncSessionToModernStore } from './storeSync'; export const BTW_SESSION_PANEL_TYPE = 'btw-session' as const; @@ -89,7 +90,6 @@ export async function openMainSession( activateWorkspace?: (workspaceId: string) => void | Promise; } ): Promise { - useSceneStore.getState().openScene('session'); appManager.updateLayout({ leftPanelActiveTab: 'sessions', leftPanelCollapsed: false, @@ -100,10 +100,13 @@ export async function openMainSession( } if (flowChatStore.getState().activeSessionId === sessionId) { - return; + syncSessionToModernStore(sessionId); + } else { + await flowChatManager.switchChatSession(sessionId); + syncSessionToModernStore(sessionId); } - await flowChatManager.switchChatSession(sessionId); + useSceneStore.getState().openScene('session'); } export function openBtwSessionInAuxPane(params: { diff --git a/src/web-ui/src/flow_chat/services/storeSync.ts b/src/web-ui/src/flow_chat/services/storeSync.ts index 7846953c..ecc98247 100644 --- a/src/web-ui/src/flow_chat/services/storeSync.ts +++ b/src/web-ui/src/flow_chat/services/storeSync.ts @@ -10,6 +10,17 @@ import { createLogger } from '@/shared/utils/logger'; const log = createLogger('StoreSync'); +function isSessionAlreadySynced( + sessionId: string, + session: object, + modernStore: ReturnType +): boolean { + return ( + modernStore.activeSession?.sessionId === sessionId && + modernStore.activeSession === session + ); +} + /** * Sync session data to new Store */ @@ -23,6 +34,9 @@ export function syncSessionToModernStore(sessionId: string): void { } const modernStore = useModernFlowChatStore.getState(); + if (isSessionAlreadySynced(sessionId, session, modernStore)) { + return; + } modernStore.setActiveSession(session); } @@ -61,9 +75,11 @@ export function startAutoSync(): () => void { lastSyncedSessionId = currentState.activeSessionId; lastSyncedSession = session; const modernStore = useModernFlowChatStore.getState(); - modernStore.setActiveSession(session); + if (!isSessionAlreadySynced(currentState.activeSessionId, session, modernStore)) { + modernStore.setActiveSession(session); + } } } return unsubscribe; -} \ No newline at end of file +} diff --git a/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts b/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts index 63eb0780..bb94d959 100644 --- a/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts @@ -9,6 +9,7 @@ import { useShallow } from 'zustand/react/shallow'; import { immer } from 'zustand/middleware/immer'; import type { Session, DialogTurn, ModelRound, FlowItem, FlowToolItem } from '../types/flow-chat'; import { isCollapsibleTool, READ_TOOL_NAMES, SEARCH_TOOL_NAMES } from '../tool-cards'; +import { flowChatStore } from './FlowChatStore'; /** * Explore group statistics (merged computed stats) @@ -272,11 +273,25 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] { return items; } +function getInitialModernState(): Pick< + ModernFlowChatState, + 'activeSession' | 'virtualItems' | 'visibleTurnInfo' +> { + const legacyState = flowChatStore.getState(); + const activeSession = legacyState.activeSessionId + ? legacyState.sessions.get(legacyState.activeSessionId) ?? null + : null; + + return { + activeSession, + virtualItems: sessionToVirtualItems(activeSession), + visibleTurnInfo: null, + }; +} + export const useModernFlowChatStore = create()( immer((set, get) => ({ - activeSession: null, - virtualItems: [], - visibleTurnInfo: null, + ...getInitialModernState(), setActiveSession: (session) => { const items = sessionToVirtualItems(session); @@ -333,4 +348,4 @@ export const useFlowChatActions = () => updateVirtualItems: state.updateVirtualItems, setVisibleTurnInfo: state.setVisibleTurnInfo, clear: state.clear, - }))); \ No newline at end of file + }))); diff --git a/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss index b8484607..471a7e3e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss @@ -156,6 +156,24 @@ z-index: 0; } +.tool-card-icon-affordance-hit { + position: absolute; + inset: 0; + z-index: 2; + margin: 0; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + appearance: none; + + &:focus-visible { + outline: 2px solid var(--color-accent-500, #60a5fa); + outline-offset: 2px; + border-radius: 6px; + } +} + /* Same square box so tool glyph and chevron share center (no circular fill). */ .tool-card-icon-marks { position: relative; diff --git a/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.tsx index 2c7a4d40..4a2ac738 100644 --- a/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.tsx @@ -132,6 +132,8 @@ export interface ToolCardHeaderProps { affordanceKind?: ToolCardHeaderAffordanceKind; /** Override context: expanded state for chevron rotation */ headerExpanded?: boolean; + /** Optional dedicated affordance click handler for the left icon rail. */ + onAffordanceClick?: (e: React.MouseEvent) => void; /** Action text */ action?: string; /** Main content */ @@ -151,6 +153,7 @@ export const ToolCardHeader: React.FC = ({ expandAffordance, affordanceKind, headerExpanded, + onAffordanceClick, action, content, extra, @@ -188,6 +191,17 @@ export const ToolCardHeader: React.FC = ({ )} + {showExpandHint && onAffordanceClick && ( +