Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useEffect, useRef, useCallback } from 'react';
import { useCanvasStore } from '../stores';
import { useApp } from '@/app/hooks/useApp';
import { TAB_EVENTS } from '../types';
import { loadPanelWidth, STORAGE_KEYS, RIGHT_PANEL_CONFIG } from '@/app/layout/panelConfig';
import { loadPanelWidth, STORAGE_KEYS, RIGHT_PANEL_CONFIG, PANEL_COMMON_CONFIG } from '@/app/layout/panelConfig';
interface UsePanelTabCoordinatorOptions {
/** Auto-collapse when all tabs are closed */
autoCollapseOnEmpty?: boolean;
Expand Down Expand Up @@ -88,7 +88,15 @@ export const usePanelTabCoordinator = (options: UsePanelTabCoordinatorOptions =
if (rightPanelCollapsedRef.current && toggleRightPanelRef.current && updateRightPanelWidth) {
// Restore last width if available, otherwise use default
const lastWidth = loadPanelWidth(STORAGE_KEYS.RIGHT_PANEL_LAST_WIDTH, RIGHT_PANEL_CONFIG.COMFORTABLE_DEFAULT);
updateRightPanelWidth(lastWidth);
// Clamp to the dynamic upper bound (session-scene container - resizer - min chat)
// so a width widened past 1200px on a larger window does not squash the chat pane
// below its one-page minimum when the panel expands on a narrower window (P2-1).
const sceneEl = document.querySelector<HTMLElement>('[data-bf-part="root"]');
const sceneWidth = sceneEl?.offsetWidth ?? 0;
const dynamicMax = sceneWidth > 0
? Math.max(RIGHT_PANEL_CONFIG.COMPACT_WIDTH, sceneWidth - PANEL_COMMON_CONFIG.RESIZER_WIDTH - PANEL_COMMON_CONFIG.MIN_CENTER_WIDTH)
: RIGHT_PANEL_CONFIG.MAX_WIDTH;
updateRightPanelWidth(Math.min(dynamicMax, Math.max(RIGHT_PANEL_CONFIG.COMPACT_WIDTH, lastWidth)));

// Expand immediately without animation (notify WorkspaceLayout)
window.dispatchEvent(new CustomEvent('expand-right-panel-immediate', {
Expand Down
8 changes: 5 additions & 3 deletions src/web-ui/src/app/hooks/useApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ export const useApp = (): UseAppReturn => {
}, []);

const updateRightPanelWidth = useCallback((width: number) => {
// Clamp width: 200px min, 1200px max
// Clamp to the right-panel compact minimum. There is no fixed MAX_WIDTH cap:
// the drag path bounds the width to the dynamic upper bound
// (container - resizer - MIN_CENTER_WIDTH) via calculateValidRightWidth, so a
// wider manual width survives and the chat pane keeps its one-page minimum.
const MIN_WIDTH = 200;
const MAX_WIDTH = 1200;
const clampedWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width));
const clampedWidth = Math.max(MIN_WIDTH, width);

appManager.updateLayout({
rightPanelWidth: clampedWidth
Expand Down
4 changes: 3 additions & 1 deletion src/web-ui/src/app/scenes/session/SessionScene.scss
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@
overflow: hidden;
position: relative;
box-sizing: border-box;
max-width: 1200px;
// No fixed max-width cap: the JS-calculated inline width (dynamic upper bound =
// container - resizer - min chat width) is authoritative, so the right panel can
// stretch until the chat pane reaches its one-page minimum.

transition: width $motion-base $easing-standard,
opacity $motion-base $easing-standard;
Expand Down
30 changes: 23 additions & 7 deletions src/web-ui/src/app/scenes/session/SessionScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ const SessionScene: React.FC<SessionSceneProps> = ({
const currentBottomHeight = state.layout.bottomTerminalPanelHeight || BOTTOM_TERMINAL_PANEL_CONFIG.COMFORTABLE_DEFAULT;
const isTerminalDockedBottom = terminalPanelPosition === 'bottom';
const isDragging = isDraggingRight || isDraggingBottom;
// Dynamic right-panel resize max: the right panel may extend past the classic
// 1200px cap up to (container - resizer - min chat width). Falls back to the
// MAX_WIDTH constant before the container is measured.
const rightPanelResizeMax = containerRef.current
? Math.max(RIGHT_PANEL_CONFIG.COMPACT_WIDTH, containerRef.current.offsetWidth - PANEL_COMMON_CONFIG.RESIZER_WIDTH - PANEL_COMMON_CONFIG.MIN_CENTER_WIDTH)
: RIGHT_PANEL_CONFIG.MAX_WIDTH;

const stopRightPanelTransition = useCallback(() => {
if (rightPanelTransitionTimerRef.current !== null) {
Expand Down Expand Up @@ -159,11 +165,12 @@ const SessionScene: React.FC<SessionSceneProps> = ({
// When the container hasn't been laid out yet (e.g. window just restored from
// minimize), offsetWidth may be 0. Bail early to avoid clamping to a tiny value.
if (containerWidth <= 0) return newWidth;
// NavPanel (240px) is outside SessionScene — only account for resizer + min chat width
// NavPanel (240px) is outside SessionScene — only account for resizer + min chat width.
// Pure dynamic upper bound (no MAX_WIDTH hard cap): the right panel stretches until
// the chat pane reaches its one-page minimum (MIN_CENTER_WIDTH).
const reserved = PANEL_COMMON_CONFIG.RESIZER_WIDTH + PANEL_COMMON_CONFIG.MIN_CENTER_WIDTH;
const dynamicMax = containerWidth - reserved;
const maxWidth = Math.min(RIGHT_PANEL_CONFIG.MAX_WIDTH, dynamicMax);
return Math.min(maxWidth, Math.max(RIGHT_PANEL_CONFIG.COMPACT_WIDTH, newWidth));
return Math.min(dynamicMax, Math.max(RIGHT_PANEL_CONFIG.COMPACT_WIDTH, newWidth));
}, []);

const calculateValidBottomHeight = useCallback((newHeight: number): number => {
Expand Down Expand Up @@ -204,7 +211,11 @@ const SessionScene: React.FC<SessionSceneProps> = ({
if (!containerRef.current) return;

const startX = e.clientX;
const startWidth = currentRightWidth;
// Start from the DOM's real width, not the (possibly stale) state value: the
// previous drag wrote the width directly to the DOM, so the state can lag
// behind a width pulled past the classic cap. Reading the DOM keeps the next
// drag starting from the actual position (no "bounce back").
const startWidth = auxPaneElementRef.current?.offsetWidth ?? currentRightWidth;
let lastValidWidth = startWidth;

setIsDraggingRight(true);
Expand Down Expand Up @@ -236,6 +247,8 @@ const SessionScene: React.FC<SessionSceneProps> = ({
if (snapped !== lastValidWidth) {
saveAndUpdateRightWidth(snapped);
} else {
// updateRightPanelWidth has no fixed cap — the effective limit is the
// dynamic bound from calculateValidRightWidth, so the wider width survives.
updateRightPanelWidth(lastValidWidth);
setLastRightWidth(lastValidWidth);
savePanelWidth(STORAGE_KEYS.RIGHT_PANEL_LAST_WIDTH, lastValidWidth);
Expand Down Expand Up @@ -376,14 +389,17 @@ const SessionScene: React.FC<SessionSceneProps> = ({
if (nowVisible && !prevVisibleRef.current) {
const saved = loadPanelWidth(STORAGE_KEYS.RIGHT_PANEL_LAST_WIDTH, currentRightWidth);
if (saved !== currentRightWidth && !state.layout.rightPanelCollapsed) {
updateRightPanelWidth(saved);
// Clamp to the dynamic upper bound so a width widened past 1200px on a
// larger window does not squash the chat pane below its one-page minimum
// after the window shrank (P2-1).
updateRightPanelWidth(calculateValidRightWidth(saved));
}
}
prevVisibleRef.current = nowVisible;
};
document.addEventListener('visibilitychange', handleVisibility);
return () => document.removeEventListener('visibilitychange', handleVisibility);
}, [currentRightWidth, updateRightPanelWidth, state.layout.rightPanelCollapsed]);
}, [currentRightWidth, calculateValidRightWidth, updateRightPanelWidth, state.layout.rightPanelCollapsed]);

// Cleanup animation frames
useEffect(() => () => {
Expand Down Expand Up @@ -553,7 +569,7 @@ const SessionScene: React.FC<SessionSceneProps> = ({
aria-label={t('layout.resizer.rightAriaLabel')}
aria-valuenow={currentRightWidth}
aria-valuemin={RIGHT_PANEL_CONFIG.COMPACT_WIDTH}
aria-valuemax={RIGHT_PANEL_CONFIG.MAX_WIDTH}
aria-valuemax={rightPanelResizeMax}
title={t('layout.resizer.title', { mode: panelModeLabels[rightPanelMode] })}
data-testid="session-right-pane-resizer"
>
Expand Down