diff --git a/src/modules/aix/client/debugger/AixDebuggerFrame.tsx b/src/modules/aix/client/debugger/AixDebuggerFrame.tsx index 66fd54663..760965daf 100644 --- a/src/modules/aix/client/debugger/AixDebuggerFrame.tsx +++ b/src/modules/aix/client/debugger/AixDebuggerFrame.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useShallow } from 'zustand/react/shallow'; import type { SxProps } from '@mui/joy/styles/types'; import { Box, Card, Chip, Divider, Sheet, Typography } from '@mui/joy'; @@ -10,6 +11,7 @@ import TimelapseIcon from '@mui/icons-material/Timelapse'; import type { AixClientDebugger } from './memstore-aix-client-debugger'; import { AixDebuggerMeasurementsTable } from './AixDebuggerMeasurementsTable'; +import { useAixClientDebuggerStore } from './memstore-aix-client-debugger'; const _styles = { @@ -79,14 +81,17 @@ export function AixDebuggerFrame(props: { frame: AixClientDebugger.Frame; }) { - // state - const [showHeaders, setShowHeaders] = React.useState(true); - const [showBody, setShowBody] = React.useState(true); - const [showParticles, setShowParticles] = React.useState(false); // hide by default (heavy) + // state: section open/close is kept in the debugger store so it persists across frame switches + const { showHeaders, showBody, showParticles, toggleOpenState } = useAixClientDebuggerStore(useShallow(state => ({ + showHeaders: !!state.openStates.headers, + showBody: !!state.openStates.body, + showParticles: !!state.openStates.particles, + toggleOpenState: state.toggleOpenState, + }))); - const handleToggleShowHeaders = React.useCallback(() => setShowHeaders(on => !on), []); - const handleToggleShowBody = React.useCallback(() => setShowBody(on => !on), []); - const handleToggleShowParticles = React.useCallback(() => setShowParticles(on => !on), []); + const handleToggleShowHeaders = React.useCallback(() => toggleOpenState('headers'), [toggleOpenState]); + const handleToggleShowBody = React.useCallback(() => toggleOpenState('body'), [toggleOpenState]); + const handleToggleShowParticles = React.useCallback(() => toggleOpenState('particles'), [toggleOpenState]); const { frame } = props; diff --git a/src/modules/aix/client/debugger/memstore-aix-client-debugger.ts b/src/modules/aix/client/debugger/memstore-aix-client-debugger.ts index 020dcba5d..ef5d45d4f 100644 --- a/src/modules/aix/client/debugger/memstore-aix-client-debugger.ts +++ b/src/modules/aix/client/debugger/memstore-aix-client-debugger.ts @@ -89,10 +89,14 @@ function _createAixClientDebuggerFrame(transport: AixClientDebugger.Transport, f /// Store /// +export type AixDebuggerOpenKey = 'headers' | 'body' | 'particles'; + interface AixClientDebuggerState { frames: AixClientDebugger.Frame[]; activeFrameId: AixFrameId | null; maxFrames: number; + // per-section open state for the current frame view + openStates: Partial>; // AIX force disable streaming for all requests (separate from per-model llmForceNoStream) aixNoStreaming: boolean; // AIX next payload override - JSON string injected into requests after validation @@ -111,6 +115,7 @@ interface AixClientDebuggerActions { setActiveFrame: (activeFrameId: AixFrameId | null) => void; setMaxFrames: (count: number) => void; clearHistory: () => void; + toggleOpenState: (key: AixDebuggerOpenKey) => void; } type AixClientDebuggerStore = AixClientDebuggerState & AixClientDebuggerActions; @@ -122,6 +127,7 @@ export const useAixClientDebuggerStore = create((_set) = frames: [], activeFrameId: null, maxFrames: DEFAULT_FRAMES_COUNT, + openStates: { headers: true, body: true }, // headers + body open by default; particles closed (heavy) aixNoStreaming: false, requestBodyOverrideJson: '', @@ -135,11 +141,16 @@ export const useAixClientDebuggerStore = create((_set) = // stealing focus from the main conversation request const isBackgroundOperation = (BACKGROUND_CONTEXT_NAMES as readonly string[]).includes(initialContext.contextName); - _set((state) => ({ - frames: [newFrame, ...state.frames].slice(0, state.maxFrames), - // Auto-select if: no active frame yet, OR this is not a background operation - activeFrameId: (!state.activeFrameId || !isBackgroundOperation) ? newFrame.id : state.activeFrameId, - })); + _set(({ activeFrameId, frames, maxFrames }) => { + // Sticky selection: only snap to the new frame if the user was following latest (nothing selected, or the current selection is the previous top frame). + const previousLatestId = frames[0]?.id ?? null; + const isFollowingLatest = activeFrameId === null || activeFrameId === previousLatestId; + const shouldAutoSelect = isFollowingLatest && !isBackgroundOperation; + return { + frames: [newFrame, ...(frames)].slice(0, maxFrames), + activeFrameId: shouldAutoSelect ? newFrame.id : activeFrameId, + }; + }); return newFrame.id; }, @@ -193,6 +204,13 @@ export const useAixClientDebuggerStore = create((_set) = activeFrameId: null, }), + toggleOpenState: (key) => _set(state => { + const next = { ...state.openStates }; + if (next[key]) delete next[key]; + else next[key] = true; + return { openStates: next }; + }), + }));