diff --git a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js
index 0f768a698..698bb90ae 100644
--- a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js
@@ -4,11 +4,8 @@ const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
+ width: 100%;
background: ${(props) => props.theme.console.contentBg};
- border-left: 1px solid ${(props) => props.theme.console.border};
- min-width: 400px;
- max-width: 600px;
- width: 40%;
overflow: hidden;
.panel-header {
diff --git a/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js
index f2149f50d..5a16352a8 100644
--- a/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js
@@ -144,6 +144,41 @@ const StyledWrapper = styled.div`
gap: 4px;
}
+ .details-panel-wrapper {
+ position: relative;
+ flex-shrink: 0;
+ height: 100%;
+ display: flex;
+ }
+
+ div.details-drag-handle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ cursor: col-resize;
+ background-color: transparent;
+ width: 6px;
+ position: absolute;
+ left: -3px;
+ top: 0;
+ z-index: 10;
+ transition: opacity 0.2s ease;
+
+ div.drag-request-border {
+ width: 1px;
+ height: 100%;
+ border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.border};
+ }
+
+ &:hover div.drag-request-border {
+ width: 1px;
+ height: 100%;
+ border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder};
+ }
+ }
+
+
.action-controls {
display: flex;
align-items: center;
diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js
index fbf4776d5..f15bf7a7f 100644
--- a/packages/bruno-app/src/components/Devtools/Console/index.js
+++ b/packages/bruno-app/src/components/Devtools/Console/index.js
@@ -23,7 +23,8 @@ import {
setActiveTab,
clearDebugErrors,
updateNetworkFilter,
- toggleAllNetworkFilters
+ toggleAllNetworkFilters,
+ updateRequestDetailsPanelWidth
} from 'providers/ReduxStore/slices/logs';
import NetworkTab from './NetworkTab';
@@ -33,6 +34,10 @@ import RequestDetailsPanel from './RequestDetailsPanel';
import ErrorDetailsPanel from './ErrorDetailsPanel';
import Performance from '../Performance';
import StyledWrapper from './StyledWrapper';
+import { useResizablePanel } from 'hooks/useResizablePanel';
+
+const MIN_DETAILS_PANEL_WIDTH = 280;
+const MAX_DETAILS_PANEL_WIDTH = 800;
const LogIcon = ({ type }) => {
const iconProps = { size: 16, strokeWidth: 1.5 };
@@ -381,8 +386,17 @@ const Console = () => {
const dispatch = useDispatch();
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
+ const savedDetailsPanelWidth = useSelector((state) => state.logs.requestDetailsPanelWidth);
const consoleRef = useRef(null);
+ const { width: detailsPanelWidth, handleDragStart: handleDetailsPanelDragStart } = useResizablePanel({
+ initialWidth: savedDetailsPanelWidth,
+ minWidth: MIN_DETAILS_PANEL_WIDTH,
+ maxWidth: MAX_DETAILS_PANEL_WIDTH,
+ direction: 'right',
+ onResizeEnd: (newWidth) => dispatch(updateRequestDetailsPanelWidth({ requestDetailsPanelWidth: newWidth }))
+ });
+
const logCounts = logs.reduce((counts, log) => {
counts[log.type] = (counts[log.type] || 0) + 1;
return counts;
@@ -614,7 +628,16 @@ const Console = () => {
{renderTabContent()}
-
+
) : activeTab === 'debug' && selectedError ? (
diff --git a/packages/bruno-app/src/hooks/useResizablePanel/index.js b/packages/bruno-app/src/hooks/useResizablePanel/index.js
new file mode 100644
index 000000000..acb988ad2
--- /dev/null
+++ b/packages/bruno-app/src/hooks/useResizablePanel/index.js
@@ -0,0 +1,69 @@
+import { useEffect, useRef, useState } from 'react';
+
+/**
+ * Drag-to-resize behavior for a side panel.
+ *
+ * @param {object} options
+ * @param {number} options.initialWidth - Starting width in px
+ * @param {number} options.minWidth - Minimum allowed width in px
+ * @param {number} options.maxWidth - Maximum allowed width in px
+ * @param {'left' | 'right'} options.direction - Panel side. 'right' means dragging
+ * left expands the panel; 'left' means dragging right expands it.
+ * @param {function} [options.onResizeEnd] - Called with the final width on mouseup
+ *
+ * @returns {{ width: number, handleDragStart: function }}
+ */
+export function useResizablePanel({
+ initialWidth,
+ minWidth,
+ maxWidth,
+ direction = 'left',
+ onResizeEnd
+}) {
+ const [width, setWidth] = useState(initialWidth);
+
+ const isDragging = useRef(false);
+ const dragStartX = useRef(0);
+ const dragStartWidth = useRef(0);
+ const currentWidth = useRef(initialWidth);
+
+ const clamp = (w) => Math.min(maxWidth, Math.max(minWidth, w));
+
+ const handleDragStart = (e) => {
+ isDragging.current = true;
+ dragStartX.current = e.clientX;
+ dragStartWidth.current = currentWidth.current;
+ e.preventDefault();
+ };
+
+ const handleMouseMove = (e) => {
+ if (!isDragging.current) return;
+ const delta
+ = direction === 'right'
+ ? dragStartX.current - e.clientX // drag left = expand
+ : e.clientX - dragStartX.current; // drag right = expand
+ const newWidth = clamp(dragStartWidth.current + delta);
+ currentWidth.current = newWidth;
+ setWidth(newWidth);
+ };
+
+ const handleMouseUp = () => {
+ if (isDragging.current) {
+ if (onResizeEnd) onResizeEnd(currentWidth.current);
+ }
+ isDragging.current = false;
+ };
+
+ useEffect(() => {
+ // Note: tying the events to the document instead of the parent to avoid fast movement
+ // from breaking the flow state of dragging
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, []);
+
+ return { width, handleDragStart };
+}
diff --git a/packages/bruno-app/src/hooks/useResizablePanel/index.spec.js b/packages/bruno-app/src/hooks/useResizablePanel/index.spec.js
new file mode 100644
index 000000000..b6a3887bd
--- /dev/null
+++ b/packages/bruno-app/src/hooks/useResizablePanel/index.spec.js
@@ -0,0 +1,192 @@
+const { describe, it, expect, jest } = require('@jest/globals');
+import { renderHook, act } from '@testing-library/react';
+import { useResizablePanel } from './index';
+
+const MIN_WIDTH = 280;
+const MAX_WIDTH = 800;
+const INITIAL_WIDTH = 400;
+
+const renderResizablePanel = ({
+ initialWidth = INITIAL_WIDTH,
+ direction = 'right',
+ onResizeEnd = jest.fn()
+} = {}) => {
+ const result = renderHook(
+ ({ initialWidth, direction, onResizeEnd }) =>
+ useResizablePanel({
+ initialWidth,
+ minWidth: MIN_WIDTH,
+ maxWidth: MAX_WIDTH,
+ direction,
+ onResizeEnd
+ }),
+ { initialProps: { initialWidth, direction, onResizeEnd } }
+ );
+ return { ...result, onResizeEnd };
+};
+
+const fireMouse = (type, clientX) => {
+ act(() => {
+ document.dispatchEvent(new MouseEvent(type, { clientX, bubbles: true }));
+ });
+};
+
+describe('useResizablePanel', () => {
+ it('returns the initial width on first render', () => {
+ const { result } = renderResizablePanel();
+ expect(result.current.width).toBe(INITIAL_WIDTH);
+ });
+
+ it('handleDragStart does not change width', () => {
+ const { result } = renderResizablePanel();
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ expect(result.current.width).toBe(INITIAL_WIDTH);
+ });
+
+ it('mousemove without an active drag is a no-op', () => {
+ const { result } = renderResizablePanel();
+
+ fireMouse('mousemove', 300);
+
+ expect(result.current.width).toBe(INITIAL_WIDTH);
+ });
+
+ it('mouseup without an active drag does not call onResizeEnd', () => {
+ const { result, onResizeEnd } = renderResizablePanel();
+
+ fireMouse('mouseup', 300);
+
+ expect(onResizeEnd).not.toHaveBeenCalled();
+ });
+
+ describe('direction: right', () => {
+ it('dragging left increases width', () => {
+ const { result } = renderResizablePanel({ direction: 'right' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 400); // moved 100px left → delta = +100
+ expect(result.current.width).toBe(INITIAL_WIDTH + 100);
+ });
+
+ it('dragging right decreases width', () => {
+ const { result } = renderResizablePanel({ direction: 'right' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 600); // moved 100px right → delta = -100
+ expect(result.current.width).toBe(INITIAL_WIDTH - 100);
+ });
+ });
+
+ describe('direction: left', () => {
+ it('dragging right increases width', () => {
+ const { result } = renderResizablePanel({ direction: 'left' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 600); // moved 100px right → delta = +100
+ expect(result.current.width).toBe(INITIAL_WIDTH + 100);
+ });
+
+ it('dragging left decreases width', () => {
+ const { result } = renderResizablePanel({ direction: 'left' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 400); // moved 100px left → delta = -100
+ expect(result.current.width).toBe(INITIAL_WIDTH - 100);
+ });
+ });
+
+ it('clamps width to minWidth', () => {
+ const { result } = renderResizablePanel({ direction: 'right' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 1000); // large rightward move → would go below minWidth
+ expect(result.current.width).toBe(MIN_WIDTH);
+ });
+
+ it('clamps width to maxWidth', () => {
+ const { result } = renderResizablePanel({ direction: 'right' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', -1000); // large leftward move → would exceed maxWidth
+ expect(result.current.width).toBe(MAX_WIDTH);
+ });
+
+ it('mouseup calls onResizeEnd with the final width', () => {
+ const { result, onResizeEnd } = renderResizablePanel({
+ direction: 'right'
+ });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 400);
+ fireMouse('mouseup', 400);
+
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ expect(onResizeEnd).toHaveBeenCalledWith(INITIAL_WIDTH + 100);
+ });
+
+ it('stops updating width after mouseup', () => {
+ const { result } = renderResizablePanel({ direction: 'right' });
+
+ act(() => {
+ result.current.handleDragStart({
+ clientX: 500,
+ preventDefault: jest.fn()
+ });
+ });
+
+ fireMouse('mousemove', 400);
+ fireMouse('mouseup', 400);
+ fireMouse('mousemove', 200); // further move after release — should be ignored
+
+ expect(result.current.width).toBe(INITIAL_WIDTH + 100);
+ });
+});
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/logs.js b/packages/bruno-app/src/providers/ReduxStore/slices/logs.js
index c55cdef50..3f88ba3d2 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/logs.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/logs.js
@@ -33,7 +33,8 @@ const initialState = {
selectedRequest: null,
selectedError: null,
maxLogs: 1000,
- maxDebugErrors: 500
+ maxDebugErrors: 500,
+ requestDetailsPanelWidth: 400
};
export const logsSlice = createSlice({
@@ -127,6 +128,9 @@ export const logsSlice = createSlice({
},
clearSelectedError: (state) => {
state.selectedError = null;
+ },
+ updateRequestDetailsPanelWidth: (state, action) => {
+ state.requestDetailsPanelWidth = action.payload.requestDetailsPanelWidth;
}
}
});
@@ -146,7 +150,8 @@ export const {
setSelectedRequest,
clearSelectedRequest,
setSelectedError,
- clearSelectedError
+ clearSelectedError,
+ updateRequestDetailsPanelWidth
} = logsSlice.actions;
export default logsSlice.reducer;