diff --git a/package-lock.json b/package-lock.json
index 86631f441..e34e6f213 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9555,6 +9555,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "license": "MIT"
+ },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@@ -10019,6 +10025,12 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/base16": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
+ "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==",
+ "license": "MIT"
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -14963,6 +14975,36 @@
"bser": "2.1.1"
}
},
+ "node_modules/fbemitter": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
+ "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "fbjs": "^3.0.0"
+ }
+ },
+ "node_modules/fbjs": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
+ "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-fetch": "^3.1.5",
+ "fbjs-css-vars": "^1.0.0",
+ "loose-envify": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "promise": "^7.1.1",
+ "setimmediate": "^1.0.5",
+ "ua-parser-js": "^1.0.35"
+ }
+ },
+ "node_modules/fbjs-css-vars": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
+ "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
+ "license": "MIT"
+ },
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -15259,6 +15301,19 @@
"node": ">=0.4.0"
}
},
+ "node_modules/flux": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz",
+ "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "fbemitter": "^3.0.0",
+ "fbjs": "^3.0.1"
+ },
+ "peerDependencies": {
+ "react": "^15.0.2 || ^16.0.0 || ^17.0.0"
+ }
+ },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -19388,6 +19443,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.curry": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
+ "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==",
+ "license": "MIT"
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -19395,6 +19456,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.flow": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
+ "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==",
+ "license": "MIT"
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -22762,6 +22829,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "license": "MIT",
+ "dependencies": {
+ "asap": "~2.0.3"
+ }
+ },
"node_modules/promise-inflight": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -22906,6 +22982,12 @@
"node": ">=6"
}
},
+ "node_modules/pure-color": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
+ "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==",
+ "license": "MIT"
+ },
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@@ -23118,6 +23200,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-base16-styling": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz",
+ "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base16": "^1.0.0",
+ "lodash.curry": "^4.0.1",
+ "lodash.flow": "^3.3.0",
+ "pure-color": "^1.2.0"
+ }
+ },
"node_modules/react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
@@ -23241,6 +23335,28 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/react-json-view": {
+ "version": "1.21.3",
+ "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz",
+ "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==",
+ "license": "MIT",
+ "dependencies": {
+ "flux": "^4.0.1",
+ "react-base16-styling": "^0.6.0",
+ "react-lifecycles-compat": "^3.0.4",
+ "react-textarea-autosize": "^8.3.2"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^16.3.0 || ^15.5.4",
+ "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4"
+ }
+ },
+ "node_modules/react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
+ "license": "MIT"
+ },
"node_modules/react-pdf": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.1.tgz",
@@ -23411,6 +23527,23 @@
}
}
},
+ "node_modules/react-textarea-autosize": {
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
+ "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "use-composed-ref": "^1.3.0",
+ "use-latest": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-tooltip": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz",
@@ -25118,7 +25251,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
- "dev": true,
"license": "MIT"
},
"node_modules/setprototypeof": {
@@ -26992,6 +27124,32 @@
"node": ">=4.2.0"
}
},
+ "node_modules/ua-parser-js": {
+ "version": "1.0.40",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
+ "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
@@ -27214,6 +27372,51 @@
}
}
},
+ "node_modules/use-composed-ref": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
+ "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
+ "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-latest": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz",
+ "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "use-isomorphic-layout-effect": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
@@ -28023,6 +28226,7 @@
"react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2",
+ "react-json-view": "^1.21.3",
"react-pdf": "9.1.1",
"react-player": "^2.16.0",
"react-redux": "^7.2.9",
diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json
index ab30dfef0..5e0f6a8ea 100644
--- a/packages/bruno-app/package.json
+++ b/packages/bruno-app/package.json
@@ -67,6 +67,7 @@
"react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2",
+ "react-json-view": "^1.21.3",
"react-pdf": "9.1.1",
"react-player": "^2.16.0",
"react-redux": "^7.2.9",
diff --git a/packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js
new file mode 100644
index 000000000..a9c71b8c2
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js
@@ -0,0 +1,163 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: ${(props) => props.theme.console.contentBg};
+ overflow: hidden;
+
+ .debug-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .debug-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: ${(props) => props.theme.console.titleColor};
+ font-size: 13px;
+ font-weight: 500;
+
+ .error-count {
+ color: ${(props) => props.theme.console.countColor};
+ font-size: 12px;
+ font-weight: 400;
+ }
+ }
+
+ .debug-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .control-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ background: transparent;
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .debug-content {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+
+ .debug-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: ${(props) => props.theme.console.emptyColor};
+ text-align: center;
+ gap: 8px;
+ padding: 40px 20px;
+
+ p {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ span {
+ font-size: 12px;
+ opacity: 0.7;
+ }
+ }
+
+ .errors-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ min-height: 0;
+ }
+
+ .errors-header {
+ display: grid;
+ grid-template-columns: 1fr 200px 120px;
+ gap: 12px;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ font-size: 11px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.titleColor};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ flex-shrink: 0;
+ }
+
+ .errors-list {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ min-height: 0;
+ }
+
+ .error-row {
+ display: grid;
+ grid-template-columns: 1fr 200px 120px;
+ gap: 12px;
+ padding: 8px 16px;
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ cursor: pointer;
+ transition: background-color 0.1s ease;
+ font-size: 12px;
+ align-items: center;
+
+ &:hover {
+ background: ${(props) => props.theme.console.logHoverBg};
+ }
+
+ &.selected {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ border-left: 3px solid ${(props) => props.theme.console.checkboxColor};
+ }
+ }
+
+ .error-message {
+ color: ${(props) => props.theme.console.messageColor};
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ }
+
+ .error-location {
+ color: ${(props) => props.theme.console.messageColor};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 11px;
+ }
+
+ .error-time {
+ color: ${(props) => props.theme.console.timestampColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 11px;
+ text-align: right;
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js b/packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js
new file mode 100644
index 000000000..cd00cd22d
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { IconBug } from '@tabler/icons';
+import {
+ setSelectedError,
+ clearDebugErrors
+} from 'providers/ReduxStore/slices/logs';
+import StyledWrapper from './StyledWrapper';
+
+const ErrorRow = ({ error, isSelected, onClick }) => {
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ return date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ fractionalSecondDigits: 3
+ });
+ };
+
+ const getShortMessage = (message, maxLength = 80) => {
+ if (!message) return 'Unknown error';
+ return message.length > maxLength ? message.substring(0, maxLength) + '...' : message;
+ };
+
+ const getLocation = (error) => {
+ if (error.filename) {
+ const filename = error.filename.split('/').pop(); // Get just the filename
+ if (error.lineno && error.colno) {
+ return `${filename}:${error.lineno}:${error.colno}`;
+ } else if (error.lineno) {
+ return `${filename}:${error.lineno}`;
+ }
+ return filename;
+ }
+ return '-';
+ };
+
+ return (
+
+
+ {getShortMessage(error.message)}
+
+
+
+ {getLocation(error)}
+
+
+
+ {formatTime(error.timestamp)}
+
+
+ );
+};
+
+const DebugTab = () => {
+ const dispatch = useDispatch();
+ const { debugErrors, selectedError } = useSelector(state => state.logs);
+
+ const handleErrorClick = (error) => {
+ dispatch(setSelectedError(error));
+ };
+
+ const handleClearErrors = () => {
+ dispatch(clearDebugErrors());
+ };
+
+ return (
+
+
+ {debugErrors.length === 0 ? (
+
+
+
No errors
+
console.error() calls will appear here
+
+ ) : (
+
+
+
Message
+
Location
+
Time
+
+
+
+ {debugErrors.map((error, index) => (
+ handleErrorClick(error)}
+ />
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+export default DebugTab;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js
new file mode 100644
index 000000000..94900df8f
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js
@@ -0,0 +1,228 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 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 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .panel-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: ${(props) => props.theme.console.titleColor};
+ font-size: 13px;
+ font-weight: 500;
+
+ .error-time {
+ color: ${(props) => props.theme.console.countColor};
+ font-size: 11px;
+ font-weight: 400;
+ }
+ }
+
+ .close-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: transparent;
+ border: none;
+ border-radius: 4px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+ }
+
+ .panel-tabs {
+ display: flex;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .tab-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 12px;
+ font-weight: 500;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+
+ &.active {
+ color: ${(props) => props.theme.console.checkboxColor};
+ border-bottom-color: ${(props) => props.theme.console.checkboxColor};
+ background: ${(props) => props.theme.console.contentBg};
+ }
+ }
+
+ .panel-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ background: ${(props) => props.theme.console.contentBg};
+ min-height: 0;
+ }
+
+ .tab-content {
+ padding: 16px;
+ height: 100%;
+ overflow-y: auto;
+ }
+
+ .section {
+ margin-bottom: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ h4 {
+ margin: 0 0 12px 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.titleColor};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+ }
+
+ .info-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .info-item {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ label {
+ font-size: 11px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.titleColor};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ span {
+ font-size: 12px;
+ color: ${(props) => props.theme.console.messageColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ word-break: break-all;
+ line-height: 1.4;
+ }
+ }
+
+ .error-message-full {
+ color: ${(props) => props.theme.console.messageColor} !important;
+ background: ${(props) => props.theme.console.headerBg};
+ padding: 8px 12px;
+ border-radius: 4px;
+ border: 1px solid ${(props) => props.theme.console.border};
+ }
+
+ .file-path {
+ color: ${(props) => props.theme.console.checkboxColor} !important;
+ font-weight: 500 !important;
+ }
+
+ .report-section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ p {
+ margin: 0;
+ font-size: 12px;
+ color: ${(props) => props.theme.console.messageColor};
+ line-height: 1.4;
+ }
+ }
+
+ .report-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 6px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 12px;
+ font-weight: 500;
+ text-decoration: none;
+ align-self: flex-start;
+
+ &:hover {
+ background: ${(props) => props.theme.console.checkboxColor};
+ color: white;
+ border-color: ${(props) => props.theme.console.checkboxColor};
+ }
+
+ span {
+ font-family: inherit;
+ }
+ }
+
+ .stack-trace-container,
+ .arguments-container {
+ background: ${(props) => props.theme.console.headerBg};
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 6px;
+ overflow: hidden;
+ }
+
+ .stack-trace,
+ .arguments {
+ margin: 0;
+ padding: 16px;
+ font-size: 11px;
+ line-height: 1.5;
+ color: ${(props) => props.theme.console.messageColor};
+ background: transparent;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ white-space: pre-wrap;
+ word-break: break-word;
+ overflow-x: auto;
+ max-height: 400px;
+ overflow-y: auto;
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js
new file mode 100644
index 000000000..91499b4a3
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js
@@ -0,0 +1,268 @@
+import React, { useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import {
+ IconX,
+ IconBug,
+ IconFileText,
+ IconCode,
+ IconStack,
+ IconBrandGithub
+} from '@tabler/icons';
+import { clearSelectedError } from 'providers/ReduxStore/slices/logs';
+import { useApp } from 'providers/App';
+import platformLib from 'platform';
+import StyledWrapper from './StyledWrapper';
+
+const ErrorInfoTab = ({ error }) => {
+ const { version } = useApp();
+
+ const formatTimestamp = (timestamp) => {
+ const date = new Date(timestamp);
+ return date.toLocaleString();
+ };
+
+ const generateGitHubIssueUrl = () => {
+ const title = `Bug: ${error.message.substring(0, 50)}${error.message.length > 50 ? '...' : ''}`;
+
+ const body = `## Bug Report
+
+### Error Details
+- **Message**: ${error.message}
+- **File**: ${error.filename || 'Unknown'}
+- **Line**: ${error.lineno || 'Unknown'}:${error.colno || 'Unknown'}
+- **Timestamp**: ${formatTimestamp(error.timestamp)}
+
+### Environment
+- **Bruno Version**: ${version}
+- **OS**: ${platformLib.os.family} ${platformLib.os.version || ''}
+- **Browser**: ${platformLib.name} ${platformLib.version || ''}
+
+### Stack Trace
+\`\`\`
+${error.stack || 'No stack trace available'}
+\`\`\`
+
+### Arguments
+\`\`\`
+${error.args ? error.args.map((arg, index) => {
+ if (arg && typeof arg === 'object' && arg.__type === 'Error') {
+ return `[${index}]: Error: ${arg.message}`;
+ }
+ return `[${index}]: ${typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)}`;
+}).join('\n') : 'No arguments'}
+\`\`\`
+
+### Steps to Reproduce
+1.
+2.
+3.
+
+### Expected Behavior
+
+
+### Additional Context
+
+`;
+
+ const encodedTitle = encodeURIComponent(title);
+ const encodedBody = encodeURIComponent(body);
+
+ return `https://github.com/usebruno/bruno/issues/new?template=BLANK_ISSUE&title=${encodedTitle}&body=${encodedBody}`;
+ };
+
+ const handleReportIssue = () => {
+ const url = generateGitHubIssueUrl();
+ window.open(url, '_blank');
+ };
+
+ return (
+
+
+
Error Information
+
+
+
+ {error.message || 'No message available'}
+
+
+ {error.filename && (
+
+
+ {error.filename}
+
+ )}
+
+ {error.lineno && (
+
+
+ {error.lineno}{error.colno ? `:${error.colno}` : ''}
+
+ )}
+
+
+
+ {formatTimestamp(error.timestamp)}
+
+
+
+
+
+
Report Issue
+
+
Found a bug? Help us improve Bruno by reporting this error on GitHub.
+
+
+
+
+ );
+};
+
+const StackTraceTab = ({ error }) => {
+ const formatStackTrace = (stack) => {
+ if (!stack) return 'Stack trace not available';
+
+ return stack
+ .split('\n')
+ .map(line => line.trim())
+ .filter(line => line.length > 0)
+ .join('\n');
+ };
+
+ return (
+
+
+
Stack Trace
+
+
+ {formatStackTrace(error.stack)}
+
+
+
+
+ );
+};
+
+const ArgumentsTab = ({ error }) => {
+ const formatArguments = (args) => {
+ if (!args || args.length === 0) return 'No arguments available';
+
+ try {
+ return args.map((arg, index) => {
+ // Handle special Error object format
+ if (arg && typeof arg === 'object' && arg.__type === 'Error') {
+ return `[${index}]: Error: ${arg.message}\n Name: ${arg.name}\n Stack: ${arg.stack || 'No stack trace'}`;
+ }
+
+ if (typeof arg === 'object' && arg !== null) {
+ return `[${index}]: ${JSON.stringify(arg, null, 2)}`;
+ }
+
+ return `[${index}]: ${String(arg)}`;
+ }).join('\n\n');
+ } catch (e) {
+ return 'Arguments could not be formatted';
+ }
+ };
+
+ return (
+
+
+
Arguments
+
+
+ {formatArguments(error.args)}
+
+
+
+
+ );
+};
+
+const ErrorDetailsPanel = () => {
+ const dispatch = useDispatch();
+ const { selectedError } = useSelector(state => state.logs);
+ const [activeTab, setActiveTab] = useState('info');
+
+ if (!selectedError) return null;
+
+ const handleClose = () => {
+ dispatch(clearSelectedError());
+ };
+
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ return date.toLocaleString();
+ };
+
+ const getTabContent = () => {
+ switch (activeTab) {
+ case 'info':
+ return ;
+ case 'stack':
+ return ;
+ case 'args':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+
+ Error Details
+ ({formatTime(selectedError.timestamp)})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {getTabContent()}
+
+
+ );
+};
+
+export default ErrorDetailsPanel;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js
new file mode 100644
index 000000000..2fc45bafe
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js
@@ -0,0 +1,293 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: ${(props) => props.theme.console.contentBg};
+ overflow: hidden;
+
+ .network-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .network-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: ${(props) => props.theme.console.titleColor};
+ font-size: 13px;
+ font-weight: 500;
+
+ .request-count {
+ color: ${(props) => props.theme.console.countColor};
+ font-size: 12px;
+ font-weight: 400;
+ }
+ }
+
+ .network-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .network-content {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-height: 0; /* Important for proper flex behavior */
+ }
+
+ .network-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: ${(props) => props.theme.console.emptyColor};
+ text-align: center;
+ gap: 8px;
+ padding: 40px 20px;
+
+ p {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ span {
+ font-size: 12px;
+ opacity: 0.7;
+ }
+ }
+
+ .requests-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ min-height: 0; /* Important for proper flex behavior */
+ }
+
+ .requests-header {
+ display: grid;
+ grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
+ gap: 12px;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ font-size: 11px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.titleColor};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ flex-shrink: 0;
+ }
+
+ .requests-list {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ min-height: 0; /* Important for proper scrolling */
+ }
+
+ .request-row {
+ display: grid;
+ grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
+ gap: 12px;
+ padding: 6px 16px;
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ cursor: pointer;
+ transition: background-color 0.1s ease;
+ font-size: 12px;
+ align-items: center;
+
+ &:hover {
+ background: ${(props) => props.theme.console.logHoverBg};
+ }
+
+ &.selected {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ border-left: 3px solid ${(props) => props.theme.console.checkboxColor};
+ }
+ }
+
+ .method-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ color: white;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ min-width: 45px;
+ }
+
+ .status-badge {
+ font-weight: 600;
+ font-size: 12px;
+ }
+
+ .request-domain {
+ color: ${(props) => props.theme.console.messageColor};
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .request-path {
+ color: ${(props) => props.theme.console.messageColor};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ }
+
+ .request-time {
+ color: ${(props) => props.theme.console.timestampColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 11px;
+ }
+
+ .request-duration {
+ color: ${(props) => props.theme.console.messageColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 11px;
+ text-align: right;
+ }
+
+ .request-size {
+ color: ${(props) => props.theme.console.messageColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 11px;
+ text-align: right;
+ }
+
+ .filter-dropdown {
+ position: relative;
+ }
+
+ .filter-dropdown-trigger {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: transparent;
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 12px;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+
+ .filter-summary {
+ font-weight: 500;
+ min-width: 24px;
+ text-align: center;
+ }
+ }
+
+ .filter-dropdown-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ right: 0;
+ min-width: 200px;
+ max-width: 250px;
+ background: ${(props) => props.theme.console.dropdownBg};
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ z-index: 1000;
+ overflow: hidden;
+ }
+
+ .filter-dropdown-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background: ${(props) => props.theme.console.dropdownHeaderBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ font-size: 12px;
+ font-weight: 500;
+ color: ${(props) => props.theme.console.titleColor};
+ }
+
+ .filter-toggle-all {
+ background: transparent;
+ border: none;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 500;
+ padding: 2px 4px;
+ border-radius: 2px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ }
+ }
+
+ .filter-dropdown-options {
+ padding: 4px 0;
+ }
+
+ .filter-option {
+ display: flex;
+ align-items: center;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.optionHoverBg};
+ }
+
+ input[type="checkbox"] {
+ margin: 0 8px 0 0;
+ width: 14px;
+ height: 14px;
+ accent-color: ${(props) => props.theme.console.checkboxColor};
+ }
+ }
+
+ .filter-option-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ }
+
+ .filter-option-label {
+ color: ${(props) => props.theme.console.optionLabelColor};
+ font-size: 12px;
+ font-weight: 400;
+ }
+
+ .filter-option-count {
+ color: ${(props) => props.theme.console.optionCountColor};
+ font-size: 11px;
+ font-weight: 400;
+ margin-left: auto;
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js
new file mode 100644
index 000000000..7501aafbd
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js
@@ -0,0 +1,302 @@
+import React, { useState, useRef, useEffect, useMemo } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import {
+ IconFilter,
+ IconChevronDown,
+ IconNetwork,
+} from '@tabler/icons';
+import {
+ updateNetworkFilter,
+ toggleAllNetworkFilters,
+ setSelectedRequest
+} from 'providers/ReduxStore/slices/logs';
+import StyledWrapper from './StyledWrapper';
+
+const MethodBadge = ({ method }) => {
+ const getMethodColor = (method) => {
+ switch (method?.toUpperCase()) {
+ case 'GET': return '#10b981';
+ case 'POST': return '#8b5cf6';
+ case 'PUT': return '#f59e0b';
+ case 'DELETE': return '#ef4444';
+ case 'PATCH': return '#06b6d4';
+ case 'HEAD': return '#6b7280';
+ case 'OPTIONS': return '#84cc16';
+ default: return '#6b7280';
+ }
+ };
+
+ return (
+
+ {method?.toUpperCase() || 'GET'}
+
+ );
+};
+
+const StatusBadge = ({ status, statusCode }) => {
+ const getStatusColor = (code) => {
+ if (code >= 200 && code < 300) return '#10b981';
+ if (code >= 300 && code < 400) return '#f59e0b';
+ if (code >= 400 && code < 500) return '#ef4444';
+ if (code >= 500) return '#dc2626';
+ return '#6b7280';
+ };
+
+ const displayStatus = statusCode || status;
+
+ return (
+
+ {displayStatus}
+
+ );
+};
+
+const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ const allFiltersEnabled = Object.values(filters).every(f => f);
+ const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+
+
+ {isOpen && (
+
+
+ Filter by Method
+
+
+
+
+ {Object.keys(filters).map(method => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const RequestRow = ({ request, isSelected, onClick }) => {
+ const { data } = request;
+ const { request: req, response: res, timestamp } = data;
+
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ return date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ fractionalSecondDigits: 3
+ });
+ };
+
+ const formatDuration = (duration) => {
+ if (!duration) return '-';
+ if (duration < 1000) return `${Math.round(duration)}ms`;
+ return `${(duration / 1000).toFixed(2)}s`;
+ };
+
+ const formatSize = (size) => {
+ if (!size) return '-';
+ if (size < 1024) return `${size}B`;
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`;
+ return `${(size / (1024 * 1024)).toFixed(1)}MB`;
+ };
+
+ const getUrl = () => {
+ return req?.url || 'Unknown URL';
+ };
+
+ const getDomain = () => {
+ try {
+ const url = new URL(getUrl());
+ return url.hostname;
+ } catch {
+ return getUrl();
+ }
+ };
+
+ const getPath = () => {
+ try {
+ const url = new URL(getUrl());
+ return url.pathname + url.search;
+ } catch {
+ return getUrl();
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {getDomain()}
+
+
+
+ {getPath()}
+
+
+
+ {formatTime(timestamp)}
+
+
+
+ {formatDuration(res?.duration)}
+
+
+
+ {formatSize(res?.size)}
+
+
+ );
+};
+
+const NetworkTab = () => {
+ const dispatch = useDispatch();
+ const { networkFilters, selectedRequest } = useSelector(state => state.logs);
+ const collections = useSelector(state => state.collections.collections);
+
+ const allRequests = useMemo(() => {
+ const requests = [];
+
+ collections.forEach(collection => {
+ if (collection.timeline) {
+ collection.timeline
+ .filter(entry => entry.type === 'request')
+ .forEach(entry => {
+ requests.push({
+ ...entry,
+ collectionName: collection.name,
+ collectionUid: collection.uid
+ });
+ });
+ }
+ });
+
+ return requests.sort((a, b) => a.timestamp - b.timestamp);
+ }, [collections]);
+
+ const filteredRequests = useMemo(() => {
+ return allRequests.filter(request => {
+ const method = request.data?.request?.method?.toUpperCase() || 'GET';
+ return networkFilters[method];
+ });
+ }, [allRequests, networkFilters]);
+
+ const requestCounts = useMemo(() => {
+ return allRequests.reduce((counts, request) => {
+ const method = request.data?.request?.method?.toUpperCase() || 'GET';
+ counts[method] = (counts[method] || 0) + 1;
+ return counts;
+ }, {});
+ }, [allRequests]);
+
+ const handleFilterToggle = (method, enabled) => {
+ dispatch(updateNetworkFilter({ method, enabled }));
+ };
+
+ const handleToggleAllFilters = (enabled) => {
+ dispatch(toggleAllNetworkFilters(enabled));
+ };
+
+ const handleRequestClick = (request) => {
+ dispatch(setSelectedRequest(request));
+ };
+
+ return (
+
+
+ {filteredRequests.length === 0 ? (
+
+
+
No network requests
+
Requests will appear here as you make API calls
+
+ ) : (
+
+
+
Method
+
Status
+
Domain
+
Path
+
Time
+
Duration
+
Size
+
+
+
+ {filteredRequests.map((request, index) => (
+ handleRequestClick(request)}
+ />
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+export default NetworkTab;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js
new file mode 100644
index 000000000..3cc9ba03d
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js
@@ -0,0 +1,347 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 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 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .panel-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: ${(props) => props.theme.console.titleColor};
+ font-size: 13px;
+ font-weight: 500;
+
+ .request-time {
+ color: ${(props) => props.theme.console.countColor};
+ font-size: 11px;
+ font-weight: 400;
+ }
+ }
+
+ .close-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: transparent;
+ border: none;
+ border-radius: 4px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+ }
+
+ .panel-tabs {
+ display: flex;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .tab-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 12px;
+ font-weight: 500;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+
+ &.active {
+ color: ${(props) => props.theme.console.checkboxColor};
+ border-bottom-color: ${(props) => props.theme.console.checkboxColor};
+ background: ${(props) => props.theme.console.contentBg};
+ }
+ }
+
+ .panel-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 16px;
+ min-height: 0;
+ height: 0;
+ }
+
+ .tab-content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ min-height: min-content;
+ }
+
+ .section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ h4 {
+ margin: 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.titleColor};
+ padding-bottom: 4px;
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ }
+ }
+
+ .info-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .info-item {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+
+ .label {
+ font-size: 11px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.countColor};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .value {
+ font-size: 12px;
+ color: ${(props) => props.theme.console.messageColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ word-break: break-all;
+ padding: 4px 8px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-radius: 4px;
+ border: 1px solid ${(props) => props.theme.console.border};
+ }
+ }
+
+ .headers-table,
+ .timeline-table {
+ overflow: auto;
+ border-radius: 4px;
+ border: 1px solid ${(props) => props.theme.console.border};
+ max-height: 300px;
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+ background: ${(props) => props.theme.console.headerBg};
+
+ thead {
+ background: ${(props) => props.theme.console.dropdownHeaderBg};
+ position: sticky;
+ top: 0;
+ z-index: 10;
+
+ td {
+ padding: 8px 12px;
+ font-weight: 600;
+ color: ${(props) => props.theme.console.titleColor};
+ text-transform: uppercase;
+ font-size: 11px;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ }
+ }
+
+ tbody {
+ tr {
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:nth-child(odd) {
+ background: ${(props) => props.theme.console.contentBg};
+ }
+
+ &:hover {
+ background: ${(props) => props.theme.console.logHoverBg};
+ }
+ }
+
+ td {
+ padding: 8px 12px;
+ vertical-align: top;
+ word-break: break-word;
+ }
+ }
+ }
+ }
+
+ .header-name,
+ .timeline-phase {
+ color: ${(props) => props.theme.console.countColor};
+ font-weight: 600;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ min-width: 120px;
+ }
+
+ .header-value,
+ .timeline-message {
+ color: ${(props) => props.theme.console.messageColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ word-break: break-all;
+ }
+
+ .timeline-duration {
+ color: ${(props) => props.theme.console.timestampColor};
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ text-align: right;
+ min-width: 80px;
+ }
+
+ .code-block {
+ background: ${(props) => props.theme.console.headerBg};
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ padding: 12px;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 11px;
+ line-height: 1.4;
+ color: ${(props) => props.theme.console.messageColor};
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-height: 400px;
+ margin: 0;
+ }
+
+ .empty-state {
+ padding: 12px;
+ text-align: center;
+ color: ${(props) => props.theme.console.emptyColor};
+ font-style: italic;
+ font-size: 12px;
+ background: ${(props) => props.theme.console.headerBg};
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ }
+
+ .response-body-container {
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ overflow: hidden;
+ background: ${(props) => props.theme.console.headerBg};
+ height: 400px;
+ display: flex;
+ flex-direction: column;
+
+ .w-full.h-full.relative.flex {
+ height: 100% !important;
+ width: 100% !important;
+ background: ${(props) => props.theme.console.headerBg} !important;
+ display: flex !important;
+ flex-direction: column !important;
+ }
+
+ div[role="tablist"] {
+ background: ${(props) => props.theme.console.dropdownHeaderBg};
+ padding: 8px 12px;
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ display: flex !important;
+ gap: 8px !important;
+ flex-wrap: wrap !important;
+ align-items: center !important;
+ min-height: 40px !important;
+ flex-shrink: 0 !important;
+
+ > div {
+ color: ${(props) => props.theme.console.buttonColor};
+ font-size: 12px !important;
+ padding: 6px 12px !important;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ cursor: pointer;
+ border: 1px solid ${(props) => props.theme.console.border};
+ background: ${(props) => props.theme.console.contentBg};
+ white-space: nowrap !important;
+ min-width: auto !important;
+ height: auto !important;
+ line-height: 1.2 !important;
+ font-weight: 500 !important;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ border-color: ${(props) => props.theme.console.buttonHoverBg};
+ }
+
+ &.active {
+ background: ${(props) => props.theme.console.checkboxColor};
+ color: white;
+ border-color: ${(props) => props.theme.console.checkboxColor};
+ }
+ }
+ }
+ .response-filter {
+ position: absolute !important;
+ bottom: 8px !important;
+ right: 8px !important;
+ left: 8px !important;
+ z-index: 10 !important;
+ }
+ }
+
+ .network-logs-container {
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ overflow: hidden;
+ background: ${(props) => props.theme.console.headerBg};
+ min-height: 200px;
+ max-height: 400px;
+
+ .network-logs {
+ background: ${(props) => props.theme.console.contentBg} !important;
+ color: ${(props) => props.theme.console.messageColor} !important;
+ height: 100% !important;
+ max-height: 400px !important;
+
+ pre {
+ color: ${(props) => props.theme.console.messageColor} !important;
+ font-size: 11px !important;
+ line-height: 1.4 !important;
+ padding: 12px !important;
+ }
+ }
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js
new file mode 100644
index 000000000..23439c959
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js
@@ -0,0 +1,242 @@
+import React, { useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import {
+ IconX,
+ IconFileText,
+ IconArrowRight,
+ IconNetwork
+} from '@tabler/icons';
+import { clearSelectedRequest } from 'providers/ReduxStore/slices/logs';
+import QueryResult from 'components/ResponsePane/QueryResult';
+import Network from 'components/ResponsePane/Timeline/TimelineItem/Network';
+import StyledWrapper from './StyledWrapper';
+import { uuid } from 'utils/common/index';
+
+const RequestTab = ({ request, response }) => {
+ const formatHeaders = (headers) => {
+ if (!headers) return [];
+ if (Array.isArray(headers)) return headers;
+ return Object.entries(headers).map(([key, value]) => ({ name: key, value }));
+ };
+
+ const formatBody = (body) => {
+ if (!body) return 'No body';
+ if (typeof body === 'string') return body;
+ return JSON.stringify(body, null, 2);
+ };
+
+ return (
+
+
+
General
+
+
+ Request URL:
+ {request?.url || 'N/A'}
+
+
+ Request Method:
+ {request?.method || 'GET'}
+
+
+
+
+
+
Request Headers
+ {formatHeaders(request?.headers).length > 0 ? (
+
+
+
+
+ | Name |
+ Value |
+
+
+
+ {formatHeaders(request.headers).map((header, index) => (
+
+ | {header.name} |
+ {header.value} |
+
+ ))}
+
+
+
+ ) : (
+
No headers
+ )}
+
+
+ {request?.body && (
+
+
Request Body
+
{formatBody(request.body)}
+
+ )}
+
+ );
+};
+
+const ResponseTab = ({ response, request, collection }) => {
+ const formatHeaders = (headers) => {
+ if (!headers) return [];
+ if (Array.isArray(headers)) return headers;
+ return Object.entries(headers).map(([key, value]) => ({ name: key, value }));
+ };
+
+ return (
+
+
+
Response Headers
+ {formatHeaders(response?.headers).length > 0 ? (
+
+
+
+
+ | Name |
+ Value |
+
+
+
+ {formatHeaders(response.headers).map((header, index) => (
+
+ | {header.name} |
+ {header.value} |
+
+ ))}
+
+
+
+ ) : (
+
No headers
+ )}
+
+
+
+
Response Body
+
+ {response?.data || response?.dataBuffer ? (
+
+ ) : (
+
No response data
+ )}
+
+
+
+ );
+};
+
+const NetworkTab = ({ response }) => {
+ const timeline = response?.timeline || [];
+
+ return (
+
+
+
Network Logs
+
+ {timeline.length > 0 ? (
+
+ ) : (
+
No network logs available
+ )}
+
+
+
+ );
+};
+
+const RequestDetailsPanel = () => {
+ const dispatch = useDispatch();
+ const { selectedRequest } = useSelector(state => state.logs);
+ const collections = useSelector(state => state.collections.collections);
+ const [activeTab, setActiveTab] = useState('request');
+
+ if (!selectedRequest) return null;
+
+ const { data } = selectedRequest;
+ const { request, response } = data;
+
+ const collection = collections.find(c => c.uid === selectedRequest.collectionUid);
+
+ const handleClose = () => {
+ dispatch(clearSelectedRequest());
+ };
+
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ return date.toLocaleString();
+ };
+
+ const getTabContent = () => {
+ switch (activeTab) {
+ case 'request':
+ return ;
+ case 'response':
+ return ;
+ case 'network':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+
+ Request Details
+ ({formatTime(selectedRequest.timestamp)})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {getTabContent()}
+
+
+ );
+};
+
+export default RequestDetailsPanel;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js
new file mode 100644
index 000000000..674214577
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js
@@ -0,0 +1,521 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ width: 100%;
+ height: 100%;
+ background: ${(props) => props.theme.console.bg};
+ border-top: 1px solid ${(props) => props.theme.console.border};
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .console-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ position: relative;
+ }
+
+ .console-tabs {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ }
+
+ .console-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 12px;
+ font-weight: 500;
+ border-radius: 4px 4px 0 0;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+
+ &.active {
+ color: ${(props) => props.theme.console.checkboxColor};
+ border-bottom-color: ${(props) => props.theme.console.checkboxColor};
+ background: ${(props) => props.theme.console.contentBg};
+ }
+ }
+
+ .console-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .console-content {
+ flex: 1;
+ overflow: hidden;
+ background: ${(props) => props.theme.console.contentBg};
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .tab-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ .tab-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ flex-shrink: 0;
+ }
+
+ .tab-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: ${(props) => props.theme.console.titleColor};
+ font-size: 13px;
+ font-weight: 500;
+
+ .log-count {
+ color: ${(props) => props.theme.console.countColor};
+ font-size: 12px;
+ font-weight: 400;
+ }
+ }
+
+ .tab-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .tab-content-area {
+ flex: 1;
+ overflow-y: auto;
+ background: ${(props) => props.theme.console.contentBg};
+ min-height: 0;
+ }
+
+ .network-with-details {
+ display: flex;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ .network-main {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .debug-with-details {
+ display: flex;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ .debug-main {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .filter-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-right: 8px;
+ padding-right: 8px;
+ border-right: 1px solid ${(props) => props.theme.console.border};
+ }
+
+ .action-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .control-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ background: transparent;
+ border: none;
+ border-radius: 4px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ }
+
+ &.close-button:hover {
+ background: #e81123;
+ color: white;
+ }
+ }
+
+ .filter-dropdown {
+ position: relative;
+ }
+
+ .filter-dropdown-trigger {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: transparent;
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 4px;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 12px;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ color: ${(props) => props.theme.console.buttonHoverColor};
+ border-color: ${(props) => props.theme.console.border};
+ }
+
+ .filter-summary {
+ font-weight: 500;
+ min-width: 24px;
+ text-align: center;
+ }
+ }
+
+ .filter-dropdown-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ min-width: 200px;
+ max-width: 250px;
+ background: ${(props) => props.theme.console.dropdownBg};
+ border: 1px solid ${(props) => props.theme.console.border};
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ z-index: 1000;
+ overflow: hidden;
+
+ &.right {
+ left: auto;
+ right: 0;
+ }
+ }
+
+ .filter-dropdown-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background: ${(props) => props.theme.console.dropdownHeaderBg};
+ border-bottom: 1px solid ${(props) => props.theme.console.border};
+ font-size: 12px;
+ font-weight: 500;
+ color: ${(props) => props.theme.console.titleColor};
+ }
+
+ .filter-toggle-all {
+ background: transparent;
+ border: none;
+ color: ${(props) => props.theme.console.buttonColor};
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 500;
+ padding: 2px 4px;
+ border-radius: 2px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.buttonHoverBg};
+ }
+ }
+
+ .filter-dropdown-options {
+ padding: 4px 0;
+ }
+
+ .filter-option {
+ display: flex;
+ align-items: center;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.optionHoverBg};
+ }
+
+ input[type="checkbox"] {
+ margin: 0 8px 0 0;
+ width: 14px;
+ height: 14px;
+ accent-color: ${(props) => props.theme.console.checkboxColor};
+ }
+ }
+
+ .filter-option-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ }
+
+ .filter-option-label {
+ color: ${(props) => props.theme.console.optionLabelColor};
+ font-size: 12px;
+ font-weight: 400;
+ }
+
+ .filter-option-count {
+ color: ${(props) => props.theme.console.optionCountColor};
+ font-size: 11px;
+ font-weight: 400;
+ margin-left: auto;
+ }
+
+ .console-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: ${(props) => props.theme.console.emptyColor};
+ text-align: center;
+ gap: 8px;
+ padding: 40px 20px;
+
+ p {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ span {
+ font-size: 12px;
+ opacity: 0.7;
+ }
+ }
+
+ .logs-container {
+ padding: 8px 0;
+ }
+
+ .method-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ color: white;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ min-width: 45px;
+ }
+
+ .log-entry {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 4px 16px;
+ font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 12px;
+ line-height: 1.4;
+ border-left: 2px solid transparent;
+ transition: background-color 0.1s ease;
+
+ &:hover {
+ background: ${(props) => props.theme.console.logHoverBg};
+ }
+
+ &.error {
+ border-left-color: #f14c4c;
+
+ .log-level {
+ background: #f14c4c;
+ color: white;
+ }
+
+ .log-icon {
+ color: #f14c4c;
+ }
+ }
+
+ &.warn {
+ border-left-color: #ffcc02;
+
+ .log-level {
+ background: #ffcc02;
+ color: #000;
+ }
+
+ .log-icon {
+ color: #ffcc02;
+ }
+ }
+
+ &.info {
+ border-left-color: #0078d4;
+
+ .log-level {
+ background: #0078d4;
+ color: white;
+ }
+
+ .log-icon {
+ color: #0078d4;
+ }
+ }
+
+ &.debug {
+ border-left-color: #9b59b6;
+
+ .log-level {
+ background: #9b59b6;
+ color: white;
+ }
+
+ .log-icon {
+ color: #9b59b6;
+ }
+ }
+
+ &.log {
+ border-left-color: #6a6a6a;
+
+ .log-level {
+ background: #6a6a6a;
+ color: white;
+ }
+
+ .log-icon {
+ color: #6a6a6a;
+ }
+ }
+ }
+
+ .log-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ min-width: 120px;
+ }
+
+ .log-timestamp {
+ color: ${(props) => props.theme.console.timestampColor};
+ font-size: 11px;
+ font-weight: 400;
+ }
+
+ .log-level {
+ font-size: 9px;
+ font-weight: 600;
+ padding: 2px 4px;
+ border-radius: 2px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .log-icon {
+ flex-shrink: 0;
+ }
+
+ .log-message {
+ color: ${(props) => props.theme.console.messageColor};
+ white-space: pre-wrap;
+ word-break: break-word;
+ flex: 1;
+
+ .log-object {
+ margin: 4px 0;
+ padding: 8px;
+ background: ${(props) => props.theme.console.headerBg};
+ border-radius: 4px;
+ border: 1px solid ${(props) => props.theme.console.border};
+
+ .react-json-view {
+ background: transparent !important;
+
+ .object-key-val {
+ font-size: 12px !important;
+ }
+
+ .object-key {
+ color: ${(props) => props.theme.console.messageColor} !important;
+ font-weight: 500 !important;
+ }
+
+ .object-value {
+ color: ${(props) => props.theme.console.messageColor} !important;
+ }
+
+ .string-value {
+ color: ${(props) => props.theme.colors?.text?.green || (props.theme.console.messageColor)} !important;
+ }
+
+ .number-value {
+ color: ${(props) => props.theme.colors?.text?.purple || (props.theme.console.messageColor)} !important;
+ }
+
+ .boolean-value {
+ color: ${(props) => props.theme.colors?.text?.yellow || (props.theme.console.messageColor)} !important;
+ }
+
+ .null-value {
+ color: ${(props) => props.theme.colors?.text?.danger || (props.theme.console.messageColor)} !important;
+ }
+
+ .object-size {
+ color: ${(props) => props.theme.console.timestampColor} !important;
+ }
+
+ .brace, .bracket {
+ color: ${(props) => props.theme.console.messageColor} !important;
+ }
+
+ .collapsed-icon, .expanded-icon {
+ color: ${(props) => props.theme.console.checkboxColor} !important;
+ }
+
+ .icon-container {
+ color: ${(props) => props.theme.console.checkboxColor} !important;
+ }
+
+ .click-to-expand, .click-to-collapse {
+ color: ${(props) => props.theme.console.checkboxColor} !important;
+ }
+ }
+ }
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js
new file mode 100644
index 000000000..bde9a8b1d
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/Console/index.js
@@ -0,0 +1,531 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import ReactJson from 'react-json-view';
+import { useTheme } from 'providers/Theme';
+import {
+ IconX,
+ IconTrash,
+ IconFilter,
+ IconAlertTriangle,
+ IconAlertCircle,
+ IconBug,
+ IconCode,
+ IconChevronDown,
+ IconTerminal2,
+ IconNetwork
+} from '@tabler/icons';
+import {
+ closeConsole,
+ clearLogs,
+ updateFilter,
+ toggleAllFilters,
+ setActiveTab,
+ clearDebugErrors,
+ updateNetworkFilter,
+ toggleAllNetworkFilters
+} from 'providers/ReduxStore/slices/logs';
+import NetworkTab from './NetworkTab';
+import RequestDetailsPanel from './RequestDetailsPanel';
+import DebugTab from './DebugTab';
+import ErrorDetailsPanel from './ErrorDetailsPanel';
+import StyledWrapper from './StyledWrapper';
+
+const LogIcon = ({ type }) => {
+ const iconProps = { size: 16, strokeWidth: 1.5 };
+
+ switch (type) {
+ case 'error':
+ return ;
+ case 'warn':
+ return ;
+ case 'info':
+ return ;
+ case 'debug':
+ return ;
+ default:
+ return ;
+ }
+};
+
+const LogTimestamp = ({ timestamp }) => {
+ const date = new Date(timestamp);
+ const time = date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ fractionalSecondDigits: 3
+ });
+
+ return {time};
+};
+
+const LogMessage = ({ message, args }) => {
+ const { displayedTheme } = useTheme();
+
+ const formatMessage = (msg, originalArgs) => {
+ if (originalArgs && originalArgs.length > 0) {
+ return originalArgs.map((arg, index) => {
+ if (typeof arg === 'object' && arg !== null) {
+ return (
+
+
+
+ );
+ }
+ return String(arg);
+ });
+ }
+ return msg;
+ };
+
+ const formattedMessage = formatMessage(message, args);
+
+ return (
+
+ {Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (
+ {item}
+ )) : formattedMessage}
+
+ );
+};
+
+const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ const allFiltersEnabled = Object.values(filters).every(f => f);
+ const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+
+
+ {isOpen && (
+
+
+ Filter by Type
+
+
+
+
+ {Object.entries(filters).map(([filterType, enabled]) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ const allFiltersEnabled = Object.values(filters).every(f => f);
+ const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
+
+ const getMethodColor = (method) => {
+ switch (method?.toUpperCase()) {
+ case 'GET': return '#10b981';
+ case 'POST': return '#8b5cf6';
+ case 'PUT': return '#f59e0b';
+ case 'DELETE': return '#ef4444';
+ case 'PATCH': return '#06b6d4';
+ case 'HEAD': return '#6b7280';
+ case 'OPTIONS': return '#84cc16';
+ default: return '#6b7280';
+ }
+ };
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+
+
+ {isOpen && (
+
+
+ Filter by Method
+
+
+
+
+ {Object.entries(filters).map(([method, enabled]) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => {
+ const logsEndRef = useRef(null);
+ const prevLogsCountRef = useRef(0);
+
+ useEffect(() => {
+ // Only scroll when new logs are added, not when switching tabs
+ if (logsEndRef.current && logs.length > prevLogsCountRef.current) {
+ logsEndRef.current.scrollIntoView({ behavior: 'auto' });
+ }
+ prevLogsCountRef.current = logs.length;
+ }, [logs]);
+
+ const filteredLogs = logs.filter(log => filters[log.type]);
+
+ return (
+
+
+ {filteredLogs.length === 0 ? (
+
+
+
No logs to display
+
Logs will appear here as your application runs
+
+ ) : (
+
+ {filteredLogs.map((log) => (
+
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+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 consoleRef = useRef(null);
+
+ const logCounts = logs.reduce((counts, log) => {
+ counts[log.type] = (counts[log.type] || 0) + 1;
+ return counts;
+ }, {});
+
+ const allRequests = React.useMemo(() => {
+ const requests = [];
+
+ collections.forEach(collection => {
+ if (collection.timeline) {
+ collection.timeline
+ .filter(entry => entry.type === 'request')
+ .forEach(entry => {
+ requests.push({
+ ...entry,
+ collectionName: collection.name,
+ collectionUid: collection.uid
+ });
+ });
+ }
+ });
+
+ return requests.sort((a, b) => a.timestamp - b.timestamp);
+ }, [collections]);
+
+ const filteredLogs = logs.filter(log => filters[log.type]);
+ const filteredRequests = allRequests.filter(request => {
+ const method = request.data?.request?.method?.toUpperCase() || 'GET';
+ return networkFilters[method];
+ });
+
+ const requestCounts = allRequests.reduce((counts, request) => {
+ const method = request.data?.request?.method?.toUpperCase() || 'GET';
+ counts[method] = (counts[method] || 0) + 1;
+ return counts;
+ }, {});
+
+ const handleFilterToggle = (filterType, enabled) => {
+ dispatch(updateFilter({ filterType, enabled }));
+ };
+
+ const handleNetworkFilterToggle = (method, enabled) => {
+ dispatch(updateNetworkFilter({ method, enabled }));
+ };
+
+ const handleClearLogs = () => {
+ dispatch(clearLogs());
+ };
+
+ const handleClearDebugErrors = () => {
+ dispatch(clearDebugErrors());
+ };
+
+ const handlecloseConsole = () => {
+ dispatch(closeConsole());
+ };
+
+ const handleToggleAllFilters = (enabled) => {
+ dispatch(toggleAllFilters(enabled));
+ };
+
+ const handleToggleAllNetworkFilters = (enabled) => {
+ dispatch(toggleAllNetworkFilters(enabled));
+ };
+
+ const handleTabChange = (tab) => {
+ dispatch(setActiveTab(tab));
+ };
+
+ const renderTabContent = () => {
+ switch (activeTab) {
+ case 'console':
+ return (
+
+ );
+ case 'network':
+ return ;
+ case 'debug':
+ return ;
+ default:
+ return (
+
+ );
+ }
+ };
+
+ const renderTabControls = () => {
+ switch (activeTab) {
+ case 'console':
+ return (
+
+ );
+ case 'network':
+ return (
+
+ );
+ case 'debug':
+ return (
+
+
+ {debugErrors.length > 0 && (
+
+ )}
+
+
+ );
+ default:
+ return null;
+ }
+ };
+
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderTabControls()}
+
+
+
+
+
+ {activeTab === 'network' && selectedRequest ? (
+
+
+ {renderTabContent()}
+
+
+
+ ) : activeTab === 'debug' && selectedError ? (
+
+
+ {renderTabContent()}
+
+
+
+ ) : (
+ renderTabContent()
+ )}
+
+
+ );
+};
+
+export default Console;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Devtools/index.js b/packages/bruno-app/src/components/Devtools/index.js
new file mode 100644
index 000000000..35c05d49a
--- /dev/null
+++ b/packages/bruno-app/src/components/Devtools/index.js
@@ -0,0 +1,89 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import Console from './Console';
+
+const MIN_DEVTOOLS_HEIGHT = 150;
+const MAX_DEVTOOLS_HEIGHT = window.innerHeight * 0.7;
+const DEFAULT_DEVTOOLS_HEIGHT = 300;
+
+const Devtools = ({ mainSectionRef }) => {
+ const isDevtoolsOpen = useSelector((state) => state.logs.isConsoleOpen);
+ const [devtoolsHeight, setDevtoolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
+ const [isResizingDevtools, setIsResizingDevtools] = useState(false);
+
+ const handleDevtoolsResizeStart = useCallback((e) => {
+ e.preventDefault();
+ setIsResizingDevtools(true);
+ }, []);
+
+ const handleDevtoolsResize = useCallback((e) => {
+ if (!isResizingDevtools || !mainSectionRef.current) return;
+
+ const windowHeight = window.innerHeight;
+ const statusBarHeight = 22;
+ const mouseY = e.clientY;
+
+ // Calculate new devtools height - expanding upward from bottom
+ const newHeight = windowHeight - mouseY - statusBarHeight;
+ const clampedHeight = Math.min(MAX_DEVTOOLS_HEIGHT, Math.max(MIN_DEVTOOLS_HEIGHT, newHeight));
+ setDevtoolsHeight(clampedHeight);
+
+ // Update main section height
+ if (mainSectionRef.current) {
+ mainSectionRef.current.style.height = `calc(100vh - 22px - ${clampedHeight}px)`;
+ }
+ }, [isResizingDevtools, mainSectionRef]);
+
+ const handleDevtoolsResizeEnd = useCallback(() => {
+ setIsResizingDevtools(false);
+ }, []);
+
+ useEffect(() => {
+ if (isResizingDevtools) {
+ document.addEventListener('mousemove', handleDevtoolsResize);
+ document.addEventListener('mouseup', handleDevtoolsResizeEnd);
+ document.body.style.userSelect = 'none';
+
+ return () => {
+ document.removeEventListener('mousemove', handleDevtoolsResize);
+ document.removeEventListener('mouseup', handleDevtoolsResizeEnd);
+ document.body.style.userSelect = '';
+ };
+ }
+ }, [isResizingDevtools, handleDevtoolsResize, handleDevtoolsResizeEnd]);
+
+ // Set initial height
+ useEffect(() => {
+ if (mainSectionRef.current && isDevtoolsOpen) {
+ mainSectionRef.current.style.height = `calc(100vh - 22px - ${devtoolsHeight}px)`;
+ }
+ }, [isDevtoolsOpen, devtoolsHeight, mainSectionRef]);
+
+ if (!isDevtoolsOpen) {
+ return null;
+ }
+
+ return (
+ <>
+ e.target.style.backgroundColor = '#0078d4'}
+ onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? '#0078d4' : 'transparent'}
+ />
+
+
+
+ >
+ );
+};
+
+export default Devtools;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/ErrorCapture/index.js b/packages/bruno-app/src/components/ErrorCapture/index.js
new file mode 100644
index 000000000..8a811276d
--- /dev/null
+++ b/packages/bruno-app/src/components/ErrorCapture/index.js
@@ -0,0 +1,147 @@
+import React, { Component, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
+import { addDebugError } from 'providers/ReduxStore/slices/logs';
+
+class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ if (this.props.onError) {
+ this.props.onError({
+ message: error.message,
+ stack: error.stack,
+ error: error,
+ timestamp: new Date().toISOString()
+ });
+ }
+
+ setTimeout(() => {
+ this.setState({ hasError: false });
+ }, 100);
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+const serializeArgs = (args) => {
+ return args.map(arg => {
+ try {
+ if (arg === null) return 'null';
+ if (arg === undefined) return 'undefined';
+ if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
+ return arg;
+ }
+ if (arg instanceof Error) {
+ return {
+ __type: 'Error',
+ name: arg.name,
+ message: arg.message,
+ stack: arg.stack
+ };
+ }
+ if (typeof arg === 'object') {
+ try {
+ return JSON.parse(JSON.stringify(arg));
+ } catch {
+ return String(arg);
+ }
+ }
+ return String(arg);
+ } catch (e) {
+ return '[Unserializable]';
+ }
+ });
+};
+
+// Helper function to extract file and line info from stack trace
+const extractFileInfo = (stack) => {
+ if (!stack) return { filename: null, lineno: null, colno: null };
+
+ try {
+ const lines = stack.split('\n');
+ for (let line of lines) {
+ if (line.includes('ErrorCapture') || line.trim() === 'Error') continue;
+
+ const match = line.match(/(?:at\s+.*?\s+)?\(?([^)]+):(\d+):(\d+)\)?/);
+ if (match) {
+ return {
+ filename: match[1],
+ lineno: parseInt(match[2]),
+ colno: parseInt(match[3])
+ };
+ }
+ }
+ } catch (e) {
+ // Ignore parsing errors
+ }
+
+ return { filename: null, lineno: null, colno: null };
+};
+
+const useGlobalErrorCapture = () => {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ const originalConsoleError = console.error;
+
+ console.error = (...args) => {
+ const currentStack = new Error().stack;
+
+ originalConsoleError.apply(console, args);
+
+ if (currentStack && currentStack.includes('useIpcEvents.js')) {
+ return;
+ }
+
+ const errorMessage = args.join(' ');
+ if (errorMessage.includes('removeConsoleLogListener')) {
+ return;
+ }
+
+ const { filename, lineno, colno } = extractFileInfo(currentStack);
+
+ const serializedArgs = serializeArgs(args);
+
+ dispatch(addDebugError({
+ message: errorMessage,
+ stack: currentStack,
+ filename: filename,
+ lineno: lineno,
+ colno: colno,
+ args: serializedArgs,
+ timestamp: new Date().toISOString()
+ }));
+ };
+
+ return () => {
+ console.error = originalConsoleError;
+ };
+ }, [dispatch]);
+};
+
+const ErrorCapture = ({ children }) => {
+ const dispatch = useDispatch();
+
+ useGlobalErrorCapture();
+
+ const handleReactError = (errorData) => {
+ dispatch(addDebugError(errorData));
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ErrorCapture;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js
index 88a28afe1..0667a4146 100644
--- a/packages/bruno-app/src/components/Notifications/index.js
+++ b/packages/bruno-app/src/components/Notifications/index.js
@@ -2,6 +2,7 @@ import { IconBell } from '@tabler/icons';
import { useState } from 'react';
import StyledWrapper from './StyleWrapper';
import Modal from 'components/Modal/index';
+import Portal from 'components/Portal';
import { useEffect } from 'react';
import { useApp } from 'providers/App';
import {
@@ -109,7 +110,7 @@ const Notifications = () => {
>
0 ? 'bell' : ''}`}
@@ -121,6 +122,7 @@ const Notifications = () => {
{showNotificationsModal && (
+
{
)}
+
)}
);
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js
index b7c8317f4..119318819 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js
@@ -113,7 +113,7 @@ const Collections = () => {
)}
-
+
{collections && collections.length
? collections.map((c) => {
return (
diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js
index 9f476e3c8..1ba71b1ab 100644
--- a/packages/bruno-app/src/components/Sidebar/index.js
+++ b/packages/bruno-app/src/components/Sidebar/index.js
@@ -1,29 +1,20 @@
import TitleBar from './TitleBar';
import Collections from './Collections';
import StyledWrapper from './StyledWrapper';
-import Preferences from 'components/Preferences';
-import Cookies from 'components/Cookies';
-import ToolHint from 'components/ToolHint';
-import GoldenEdition from './GoldenEdition';
import { useApp } from 'providers/App';
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { IconSettings, IconCookie, IconHeart } from '@tabler/icons';
-import { updateLeftSidebarWidth, updateIsDragging, showPreferences } from 'providers/ReduxStore/slices/app';
+import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';
import { useTheme } from 'providers/Theme';
-import Notifications from 'components/Notifications';
const MIN_LEFT_SIDEBAR_WIDTH = 221;
const MAX_LEFT_SIDEBAR_WIDTH = 600;
const Sidebar = () => {
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
- const preferencesOpen = useSelector((state) => state.app.showPreferences);
- const [goldenEditionOpen, setGoldenEditionOpen] = useState(false);
const { version } = useApp();
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
- const [cookiesOpen, setCookiesOpen] = useState(false);
const { storedTheme } = useTheme();
@@ -81,112 +72,14 @@ const Sidebar = () => {
}, [leftSidebarWidth]);
return (
-
+