Skip to content

fix: export video CSP preview, code panel UX, and output dock visibility - #191

Merged
ayoub3bidi merged 4 commits into
developfrom
fix/code-panel-ux-and-export-csp
Jun 27, 2026
Merged

fix: export video CSP preview, code panel UX, and output dock visibility#191
ayoub3bidi merged 4 commits into
developfrom
fix/code-panel-ux-and-export-csp

Conversation

@ayoub3bidi

@ayoub3bidi ayoub3bidi commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes two production UX bugs and polishes the Python code side panel. Exported MP4 downloads worked but in-app preview failed on deployed sites because CSP blocked blob: URLs for <video> elements. The code panel had background scroll bleed while open, a collapsed output section that disappeared off-screen, and a Run toolbar that broke visual continuity with the tab switchers.


What was done

Export video preview (CSP)

Code panel UX

  • Add useBodyScrollLock in VisualizerApp while the Python or insight panel is open (preserves scroll position on close)
  • Add overscroll-contain on PythonCodePanel and AlgorithmInsightPanel
  • Move Run / Reset into the OutputConsole header (Output / Test Cases dock); remove the standalone toolbar between tabs and Monaco
  • Match Pseudocode tab spacing: p-2 gap between tab switchers and Monaco editor
  • Collapsed output: dedicated “Output & Test Cases” dock banner with expand control; Run stays available when collapsed
  • Fix panel sizing: desktop panel uses top-14 bottom-0 instead of h-full + top: 56px (panel was 56px taller than the viewport, hiding the bottom dock)
  • Render collapsed dock outside the overflow-hidden editor container so it stays pinned and visible
  • Run button shows icon-only spinner (loading) or animated dots (running); full label kept in aria-label for accessibility

i18n

  • New strings in en / fr / ar: output_dock_label, expand_output, collapse_output

Technical Details

Area Root cause Fix
Video preview empty No media-src; browser fell back to default-src 'self' and blocked blob: Explicit media-src 'self' blob:
Download worked <a download> is not governed by media-src No app code change needed
Output dock vanished h-full + top: 56px pushed panel bottom below viewport; flex + overflow-hidden clipped collapsed header top-14 bottom-0; collapsed dock as sibling below editor stack
Page scroll behind panel No body scroll lock on overlay open useBodyScrollLock with fixed body + scroll restoration

Monaco Ctrl/Cmd+Enter run shortcut unchanged. Test Cases Run tests button in TestCasesPanel unchanged (separate from code Run).


Testing

  • pnpm vitest run src/security/cspHeaders.test.js
  • pnpm vitest run src/hooks/useBodyScrollLock.test.js
  • pnpm vitest run src/components/OutputConsole.test.jsx src/components/PythonCodePanel.test.jsx
  • pnpm build (validates dist/_headers CSP directives)

Manual verification

Export (dev/staging with CSP headers):

  1. Export a video → preview plays in modal after render
  2. Download still works
  3. Console: no blob: / media-src CSP violation

Code panel (/app):

  1. Open code panel → scroll wheel on backdrop does not move page; scroll restored on close
  2. Same for insight panel
  3. Python tab: small gap under tabs; Monaco flush (no middle Run row)
  4. Run from output dock header; Reset after editing code
  5. Collapse output → Output & Test Cases banner visible at bottom → expand works
  6. Run while loading: spinner only on button
  7. Arabic RTL: editor/output stack remains LTR; labels translated

Notes

  • Local pnpm dev does not send CSP headers; export preview CSP fix applies on Cloudflare/Netlify builds only
  • PR_DESCRIPTION.md is for review handoff; exclude from commit unless you want it tracked in-repo

Related

  • Export preview CSP analysis: blocked blob: under default-src fallback when media-src was unset
  • Code panel plan: scroll lock, collapsed dock banner, Run in output header (Option 2)

Summary by CodeRabbit

  • New Features
    • Added clearer Run/Reset controls and improved expanded/collapsed “Output & Test Cases” dock behavior in the Python code panel.
    • Improved panel scrolling by preventing overscroll.
  • Bug Fixes
    • Updated Content Security Policy to allow required external connections for video export.
    • Refined output console status indicators (removed the “loading runtime” badge).
  • Localization
    • Added new output dock and expand/collapse translation strings (EN/FR/AR).
  • Tests
    • Expanded UI and security validation tests for console controls and scroll-lock behavior.

@ayoub3bidi ayoub3bidi self-assigned this Jun 27, 2026
@netlify

netlify Bot commented Jun 27, 2026

Copy link
Copy Markdown

Deploy Preview for dev-bayanflow ready!

Name Link
🔨 Latest commit 3d14525
🔍 Latest deploy log https://app.netlify.com/projects/dev-bayanflow/deploys/6a3fd9e4a052e80008565708
😎 Deploy Preview https://deploy-preview-191--dev-bayanflow.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added style Improve styling, design, and animation config tests labels Jun 27, 2026
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Run/Reset controls to the output console, refactors the Python panel layout, locks body scrolling when panels open, updates translations, and adds CSP parsing and validation for the Remotion origin.

Changes

Output panel and page behavior

Layer / File(s) Summary
OutputConsole controls
src/components/OutputConsole.jsx, src/components/OutputConsole.test.jsx
Adds onRun, onReset, and isModified, renders Run and Reset controls in collapsed and expanded states, updates labels and badge rendering, and refactors output content rendering.
PythonCodePanel layout
src/components/PythonCodePanel.jsx, src/components/PythonCodePanel.test.jsx
Removes the editor toolbar Run and Reset controls, restructures the split layout, updates panel sizing, and passes the new output-console props through expanded and collapsed render paths.
Scroll lock and overscroll
src/hooks/useBodyScrollLock.js, src/hooks/useBodyScrollLock.test.js, src/pages/VisualizerApp.jsx, src/components/AlgorithmInsightPanel.jsx
Adds body scroll locking with restore behavior, wires it into VisualizerApp, and adds overscroll-contain to both AlgorithmInsightPanel variants.
Test harness state resets
src/pages/VisualizerApp.test.jsx, src/test/setup.js, src/test/soundManagerMock.js
Adds reset hooks for body styles, localStorage, and sound manager mock state across test runs.
Output panel translations
src/i18n/locales/ar/translation.json, src/i18n/locales/en/translation.json, src/i18n/locales/fr/translation.json
Adds expand, collapse, and output-dock translation keys in Arabic, English, and French.

CSP video-export enforcement

Layer / File(s) Summary
Header CSP updates
public/_headers, netlify.toml
Adds https://www.remotion.pro to connect-src in both header files.
CSP parsing helpers
scripts/cspHeaders.js
Adds CSP directive parsing, header extraction helpers, and directive assertions for media-src and connect-src.
Build and test CSP validation
vite.config.js, src/security/cspHeaders.test.js
Validates dist/_headers during build and checks the CSP directives in public/_headers and netlify.toml in tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

ci

Poem

🐇 I hop where panels open wide,
Run and Reset now dance inside.
The scroll stays still, the headers glide,
CSP and tests ride side by side.
A little bunny cheers, “Hooray!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main CSP and Python code panel/output dock changes without misleading details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/code-panel-ux-and-export-csp

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/cspHeaders.js`:
- Around line 64-75: The CSP validation in cspHeaders.js is matching sources
with substring checks, so values like a longer token can incorrectly satisfy the
guard. Update the mediaSrc and connectSrc validation to parse each directive
into individual tokens and check exact membership for 'self', blob:, and
https://www.remotion.pro rather than using includes() on the full string. Keep
the existing error messages and source context, but ensure the logic only passes
when the required CSP source is present as a distinct token.

In `@src/components/OutputConsole.jsx`:
- Line 179: The only issue here is trailing whitespace in the OutputConsole
component. Remove the extra spaces on the blank/indented line in
OutputConsole.jsx so the formatting passes quality checks, and keep the
surrounding JSX/markup unchanged.
- Around line 90-97: The run button label in OutputConsole should not depend on
output or error presence, since successful no-stdout executions still need to
show the post-run action. Update the runLabel logic in OutputConsole.jsx to base
the Run/Rerun text on execution state from status, or an explicit hasRun flag if
available, so completed runs always show Rerun even when output and error are
empty. Keep the loading/running branches unchanged and adjust the final fallback
to reflect whether the console has already executed.

In `@src/components/PythonCodePanel.jsx`:
- Around line 596-629: The collapse/expand logic in PythonCodePanel is
remounting OutputConsole and resetting its internal tab state. Update the
OutputConsole usage so there is only one mounted instance across
expanded/collapsed states, or lift the tab state out of OutputConsole into
PythonCodePanel and pass it through props. Keep the fix centered on the
OutputConsole render paths and the isOutputExpanded toggle behavior so the
selected tab and other dock state persist after collapsing and re-expanding.
- Around line 499-516: The direction override is applied too broadly in
PythonCodePanel, causing OutputConsole and TestCasesPanel to inherit LTR and
lose RTL layout. Move the dir="ltr" override so it only wraps the Monaco editor
container, and leave the surrounding flex stack to inherit the app’s direction.
Use the existing PythonCodePanel structure and the editor container around the
code area to scope the change without affecting the output panels.

In `@src/components/PythonCodePanel.test.jsx`:
- Around line 703-709: The current assertion in PythonCodePanel.test.jsx is too
broad and can miss the toolbar regression because tablist.nextElementSibling may
still contain a standalone Run row. Update the test around the
tablist/editor/output assertions to explicitly verify that any Run button is
only rendered inside the output-console area, using the existing tablist,
monaco-editor, and output-console selectors to locate the elements.

In `@src/hooks/useBodyScrollLock.js`:
- Around line 7-44: Switch useBodyScrollLock from useEffect to useLayoutEffect
so the body style changes in the useBodyScrollLock hook are applied before paint
and avoid a scrollbar/layout flash. Update the React import in useBodyScrollLock
and keep the existing lock/unlock logic, including the document/window guards
and cleanup that restores body styles and scroll position.

In `@vite.config.js`:
- Around line 112-113: The CSP validation in the Vite build hook has a blind
spot when dist/_headers is missing, so the build can pass without running any
checks. Update the hook around extractCspFromHeadersFile and
assertVideoExportCspDirectives to explicitly verify that dist/_headers was
emitted before reading it, and fail the build with a clear error if it is
absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f288b01-a8cc-4060-acb7-2eed06b3ff9b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fadbdd and 73ac4a4.

📒 Files selected for processing (16)
  • netlify.toml
  • public/_headers
  • scripts/cspHeaders.js
  • src/components/AlgorithmInsightPanel.jsx
  • src/components/OutputConsole.jsx
  • src/components/OutputConsole.test.jsx
  • src/components/PythonCodePanel.jsx
  • src/components/PythonCodePanel.test.jsx
  • src/hooks/useBodyScrollLock.js
  • src/hooks/useBodyScrollLock.test.js
  • src/i18n/locales/ar/translation.json
  • src/i18n/locales/en/translation.json
  • src/i18n/locales/fr/translation.json
  • src/pages/VisualizerApp.jsx
  • src/security/cspHeaders.test.js
  • vite.config.js

Comment thread scripts/cspHeaders.js
Comment on lines +64 to +75
const mediaSrc = directives.get('media-src');
if (!mediaSrc?.includes("'self'") || !mediaSrc.includes('blob:')) {
throw new Error(
`${source}: media-src must include 'self' and blob: (required for export preview)`
);
}

const connectSrc = directives.get('connect-src');
if (!connectSrc?.includes('https://www.remotion.pro')) {
throw new Error(
`${source}: connect-src must include https://www.remotion.pro (Remotion telemetry)`
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Match CSP sources by token, not substring.

These includes() checks can pass on the wrong source expression, so the new build/test guard can report success even when the required CSP source is not actually present.

Suggested fix
 export function assertVideoExportCspDirectives(csp, source) {
   const directives = parseCspDirectives(csp);
+  const tokens = (value) => (value ? value.split(/\s+/).filter(Boolean) : []);

   const mediaSrc = directives.get('media-src');
-  if (!mediaSrc?.includes("'self'") || !mediaSrc.includes('blob:')) {
+  const mediaTokens = tokens(mediaSrc);
+  if (!mediaTokens.includes("'self'") || !mediaTokens.includes('blob:')) {
     throw new Error(
       `${source}: media-src must include 'self' and blob: (required for export preview)`
     );
   }

   const connectSrc = directives.get('connect-src');
-  if (!connectSrc?.includes('https://www.remotion.pro')) {
+  const connectTokens = tokens(connectSrc);
+  if (!connectTokens.includes('https://www.remotion.pro')) {
     throw new Error(
       `${source}: connect-src must include https://www.remotion.pro (Remotion telemetry)`
     );
   }

   return { mediaSrc, connectSrc: connectSrc ?? '' };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mediaSrc = directives.get('media-src');
if (!mediaSrc?.includes("'self'") || !mediaSrc.includes('blob:')) {
throw new Error(
`${source}: media-src must include 'self' and blob: (required for export preview)`
);
}
const connectSrc = directives.get('connect-src');
if (!connectSrc?.includes('https://www.remotion.pro')) {
throw new Error(
`${source}: connect-src must include https://www.remotion.pro (Remotion telemetry)`
);
const mediaSrc = directives.get('media-src');
const tokens = (value) => (value ? value.split(/\s+/).filter(Boolean) : []);
const mediaTokens = tokens(mediaSrc);
if (!mediaTokens.includes("'self'") || !mediaTokens.includes('blob:')) {
throw new Error(
`${source}: media-src must include 'self' and blob: (required for export preview)`
);
}
const connectSrc = directives.get('connect-src');
const connectTokens = tokens(connectSrc);
if (!connectTokens.includes('https://www.remotion.pro')) {
throw new Error(
`${source}: connect-src must include https://www.remotion.pro (Remotion telemetry)`
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/cspHeaders.js` around lines 64 - 75, The CSP validation in
cspHeaders.js is matching sources with substring checks, so values like a longer
token can incorrectly satisfy the guard. Update the mediaSrc and connectSrc
validation to parse each directive into individual tokens and check exact
membership for 'self', blob:, and https://www.remotion.pro rather than using
includes() on the full string. Keep the existing error messages and source
context, but ensure the logic only passes when the required CSP source is
present as a distinct token.

Comment on lines +90 to +97
const runLabel =
status === 'loading'
? t('python_code.loading_runtime', { defaultValue: 'Loading Python...' })
: status === 'running'
? t('python_code.running', { defaultValue: 'Running...' })
: output || error
? t('python_code.rerun', { defaultValue: 'Rerun' })
: t('python_code.run', { defaultValue: 'Run' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Base the Run/Rerun label on execution state, not console content.

A successful run with no stdout leaves both output and error empty, so this branch falls back to "Run" even after execution completed. That makes the new action text inconsistent for valid no-output programs. Key it off terminal status (or an explicit hasRun flag) instead.

Suggested fix
-  const runLabel =
-    status === 'loading'
-      ? t('python_code.loading_runtime', { defaultValue: 'Loading Python...' })
-      : status === 'running'
-        ? t('python_code.running', { defaultValue: 'Running...' })
-        : output || error
-          ? t('python_code.rerun', { defaultValue: 'Rerun' })
-          : t('python_code.run', { defaultValue: 'Run' });
+  const hasRun =
+    status === 'success' || status === 'error' || status === 'timeout';
+
+  const runLabel =
+    status === 'loading'
+      ? t('python_code.loading_runtime', { defaultValue: 'Loading Python...' })
+      : status === 'running'
+        ? t('python_code.running', { defaultValue: 'Running...' })
+        : hasRun
+          ? t('python_code.rerun', { defaultValue: 'Rerun' })
+          : t('python_code.run', { defaultValue: 'Run' });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const runLabel =
status === 'loading'
? t('python_code.loading_runtime', { defaultValue: 'Loading Python...' })
: status === 'running'
? t('python_code.running', { defaultValue: 'Running...' })
: output || error
? t('python_code.rerun', { defaultValue: 'Rerun' })
: t('python_code.run', { defaultValue: 'Run' });
const hasRun =
status === 'success' || status === 'error' || status === 'timeout';
const runLabel =
status === 'loading'
? t('python_code.loading_runtime', { defaultValue: 'Loading Python...' })
: status === 'running'
? t('python_code.running', { defaultValue: 'Running...' })
: hasRun
? t('python_code.rerun', { defaultValue: 'Rerun' })
: t('python_code.run', { defaultValue: 'Run' });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/OutputConsole.jsx` around lines 90 - 97, The run button label
in OutputConsole should not depend on output or error presence, since successful
no-stdout executions still need to show the post-run action. Update the runLabel
logic in OutputConsole.jsx to base the Run/Rerun text on execution state from
status, or an explicit hasRun flag if available, so completed runs always show
Rerun even when output and error are empty. Keep the loading/running branches
unchanged and adjust the final fallback to reflect whether the console has
already executed.

Comment thread src/components/OutputConsole.jsx Outdated
Comment thread src/components/PythonCodePanel.jsx Outdated
Comment on lines +499 to +516
<div
ref={resizeContainerRef}
className="flex-1 min-h-0 flex flex-col overflow-hidden relative"
className="flex-1 min-h-0 flex flex-col p-2"
dir="ltr"
>
<div
className={`min-h-0 overflow-hidden ${isMobile ? 'touch-pan-y' : ''}`}
style={{
flex:
isMobile || !isOutputExpanded
? '1 1 0'
: `0 0 calc(${100 - outputHeightPercent}% - 2px)`,
minHeight: isMobile ? 100 : 80,
}}
ref={resizeContainerRef}
className="min-h-0 flex flex-col overflow-hidden flex-1"
>
<div
className={`min-h-0 overflow-hidden ${isMobile ? 'touch-pan-y' : ''}`}
style={{
flex:
isMobile || !isOutputExpanded
? '1 1 0'
: `0 0 calc(${100 - outputHeightPercent}% - 2px)`,
minHeight: isMobile ? 100 : 80,
}}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't force the whole editor/output stack to LTR.

Line 501 now makes OutputConsole and TestCasesPanel inherit dir="ltr", so Arabic dock labels/layout lose RTL behavior. Scope the direction override to the Monaco container only.

Suggested fix
-                  <div
-                    className="flex-1 min-h-0 flex flex-col p-2"
-                    dir="ltr"
-                  >
+                  <div className="flex-1 min-h-0 flex flex-col p-2">
                     <div
                       ref={resizeContainerRef}
                       className="min-h-0 flex flex-col overflow-hidden flex-1"
                     >
                       <div
+                        dir="ltr"
                         className={`min-h-0 overflow-hidden ${isMobile ? 'touch-pan-y' : ''}`}
                         style={{
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
ref={resizeContainerRef}
className="flex-1 min-h-0 flex flex-col overflow-hidden relative"
className="flex-1 min-h-0 flex flex-col p-2"
dir="ltr"
>
<div
className={`min-h-0 overflow-hidden ${isMobile ? 'touch-pan-y' : ''}`}
style={{
flex:
isMobile || !isOutputExpanded
? '1 1 0'
: `0 0 calc(${100 - outputHeightPercent}% - 2px)`,
minHeight: isMobile ? 100 : 80,
}}
ref={resizeContainerRef}
className="min-h-0 flex flex-col overflow-hidden flex-1"
>
<div
className={`min-h-0 overflow-hidden ${isMobile ? 'touch-pan-y' : ''}`}
style={{
flex:
isMobile || !isOutputExpanded
? '1 1 0'
: `0 0 calc(${100 - outputHeightPercent}% - 2px)`,
minHeight: isMobile ? 100 : 80,
}}
>
<div className="flex-1 min-h-0 flex flex-col p-2">
<div
ref={resizeContainerRef}
className="min-h-0 flex flex-col overflow-hidden flex-1"
>
<div
dir="ltr"
className={`min-h-0 overflow-hidden ${isMobile ? 'touch-pan-y' : ''}`}
style={{
flex:
isMobile || !isOutputExpanded
? '1 1 0'
: `0 0 calc(${100 - outputHeightPercent}% - 2px)`,
minHeight: isMobile ? 100 : 80,
}}
>
🧰 Tools
🪛 GitHub Check: Code Quality

[failure] 499-499:
Replace ⏎····················className="flex-1·min-h-0·flex·flex-col·p-2"⏎····················dir="ltr"⏎·················· with ·className="flex-1·min-h-0·flex·flex-col·p-2"·dir="ltr"

🪛 GitHub Check: Upload PR preview

[failure] 499-499:
Replace ⏎····················className="flex-1·min-h-0·flex·flex-col·p-2"⏎····················dir="ltr"⏎·················· with ·className="flex-1·min-h-0·flex·flex-col·p-2"·dir="ltr"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PythonCodePanel.jsx` around lines 499 - 516, The direction
override is applied too broadly in PythonCodePanel, causing OutputConsole and
TestCasesPanel to inherit LTR and lose RTL layout. Move the dir="ltr" override
so it only wraps the Monaco editor container, and leave the surrounding flex
stack to inherit the app’s direction. Use the existing PythonCodePanel structure
and the editor container around the code area to scope the change without
affecting the output panels.

Comment thread src/components/PythonCodePanel.jsx Outdated
Comment on lines +596 to +629
{isOutputExpanded && (
<div
className="min-h-0 overflow-hidden flex flex-col shrink-0"
style={{
flex: isMobile
? '0 0 auto'
: `0 0 calc(${outputHeightPercent}% - 2px)`,
minHeight:
isOutputExpanded && !isMobile ? 80 : undefined,
}}
>
<OutputConsole
status={status}
output={output}
error={error}
onClear={clearOutput}
isExpanded={isOutputExpanded}
onToggleExpand={() =>
setIsOutputExpanded(prev => !prev)
}
testCases={testCases}
testResults={testResults}
testStatus={testStatus}
testError={testError}
onRunTests={handleRunTests}
onAddTestCase={handleAddTestCase}
onEditTestCase={handleEditTestCase}
onDeleteTestCase={handleDeleteTestCase}
onClearTestResults={clearTestResults}
/>
minHeight: !isMobile ? 80 : undefined,
}}
>
<OutputConsole
status={status}
output={output}
error={error}
onClear={clearOutput}
isExpanded={isOutputExpanded}
onToggleExpand={() =>
setIsOutputExpanded(prev => !prev)
}
onRun={handleRun}
onReset={() => setCode(pythonCode)}
isModified={isModified}
testCases={testCases}
testResults={testResults}
testStatus={testStatus}
testError={testError}
onRunTests={handleRunTests}
onAddTestCase={handleAddTestCase}
onEditTestCase={handleEditTestCase}
onDeleteTestCase={handleDeleteTestCase}
onClearTestResults={clearTestResults}
/>
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Collapse/expand remounts the console and drops its tab state.

These branches mount different OutputConsole instances. Since OutputConsole keeps internalTab internally, collapsing while the user is on Test Cases re-expands back on Output, and any other uncontrolled dock state is lost. Keep a single instance or lift activeTab into PythonCodePanel.

Suggested fix
+  const [outputTab, setOutputTab] = useState('output');
+
                     {isOutputExpanded && (
                       <div
                         className="min-h-0 overflow-hidden flex flex-col shrink-0"
                         style={{
@@
                         <OutputConsole
+                          activeTab={outputTab}
+                          onTabChange={setOutputTab}
                           status={status}
                           output={output}
@@
                         <OutputConsole
+                          activeTab={outputTab}
+                          onTabChange={setOutputTab}
                           status={status}
                           output={output}

Also applies to: 631-655

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 615-615: Avoid using the initial state variable in setState
Context: setCode(pythonCode)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PythonCodePanel.jsx` around lines 596 - 629, The
collapse/expand logic in PythonCodePanel is remounting OutputConsole and
resetting its internal tab state. Update the OutputConsole usage so there is
only one mounted instance across expanded/collapsed states, or lift the tab
state out of OutputConsole into PythonCodePanel and pass it through props. Keep
the fix centered on the OutputConsole render paths and the isOutputExpanded
toggle behavior so the selected tab and other dock state persist after
collapsing and re-expanding.

Comment on lines +703 to +709
const tablist = screen.getByRole('tablist');
const editor = screen.getByTestId('monaco-editor');
const tablistParent = tablist.parentElement;
const editorContainer = editor.parentElement?.parentElement;

expect(tablistParent).toContainElement(editorContainer);
expect(tablist.nextElementSibling).not.toHaveTextContent(/^Run$/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This assertion won't catch the toolbar regression.

tablist.nextElementSibling is the whole editor/output wrapper, and not.toHaveTextContent(/^Run$/) only fails when that wrapper's entire text is exactly Run. A standalone Run row above the editor would still pass. Assert that the only Run button lives inside output-console instead.

Suggested fix
-      expect(tablist.nextElementSibling).not.toHaveTextContent(/^Run$/);
+      const runButtons = screen.getAllByRole('button', { name: /^run$/i });
+      expect(runButtons).toHaveLength(1);
+      expect(screen.getByTestId('output-console')).toContainElement(
+        runButtons[0]
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tablist = screen.getByRole('tablist');
const editor = screen.getByTestId('monaco-editor');
const tablistParent = tablist.parentElement;
const editorContainer = editor.parentElement?.parentElement;
expect(tablistParent).toContainElement(editorContainer);
expect(tablist.nextElementSibling).not.toHaveTextContent(/^Run$/);
const tablist = screen.getByRole('tablist');
const editor = screen.getByTestId('monaco-editor');
const tablistParent = tablist.parentElement;
const editorContainer = editor.parentElement?.parentElement;
expect(tablistParent).toContainElement(editorContainer);
const runButtons = screen.getAllByRole('button', { name: /^run$/i });
expect(runButtons).toHaveLength(1);
expect(screen.getByTestId('output-console')).toContainElement(
runButtons[0]
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PythonCodePanel.test.jsx` around lines 703 - 709, The current
assertion in PythonCodePanel.test.jsx is too broad and can miss the toolbar
regression because tablist.nextElementSibling may still contain a standalone Run
row. Update the test around the tablist/editor/output assertions to explicitly
verify that any Run button is only rendered inside the output-console area,
using the existing tablist, monaco-editor, and output-console selectors to
locate the elements.

Comment on lines +7 to +44
import { useEffect } from 'react';

/**
* Prevent the document from scrolling while an overlay (side panel, modal) is open.
* Preserves scroll position using position:fixed on body.
*
* @param {boolean} isLocked - Whether scroll should be locked
*/
export function useBodyScrollLock(isLocked) {
useEffect(() => {
if (
!isLocked ||
typeof window === 'undefined' ||
typeof document === 'undefined'
) {
return;
}

const scrollY = window.scrollY;
const { body } = document;
const previousPosition = body.style.position;
const previousTop = body.style.top;
const previousWidth = body.style.width;
const previousOverflow = body.style.overflow;

body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.width = '100%';
body.style.overflow = 'hidden';

return () => {
body.style.position = previousPosition;
body.style.top = previousTop;
body.style.width = previousWidth;
body.style.overflow = previousOverflow;
window.scrollTo(0, scrollY);
};
}, [isLocked]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## useBodyScrollLock.js\n'
sed -n '1,220p' src/hooks/useBodyScrollLock.js

printf '\n## VisualizerApp.jsx references\n'
rg -n "useBodyScrollLock|scroll lock|body scroll" src/pages/VisualizerApp.jsx src -g '!**/node_modules/**'

printf '\n## project markers\n'
git ls-files | rg '^(package.json|vite.config|next.config|remix.config|astro.config|gatsby-config|src/|app/|pages/|public/)' | sed -n '1,160p'

Repository: ayoub3bidi/bayan-flow

Length of output: 8965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## package.json\n'
sed -n '1,240p' package.json

printf '\n## app entry points\n'
git ls-files src | rg '(^src/(main|index)\.(jsx|js|tsx|ts)$|^src/App\.(jsx|js|tsx|ts)$|^src/pages/VisualizerApp\.jsx$|^src/components/.*\.(jsx|js|tsx|ts)$)' | sed -n '1,120p'

printf '\n## useLayoutEffect usages\n'
rg -n "useLayoutEffect" src package.json

printf '\n## Vite/SSR markers\n'
rg -n "createRoot|hydrateRoot|ReactDOM|SSR|server-side|useLayoutEffect" src vite.config.* package.json

Repository: ayoub3bidi/bayan-flow

Length of output: 6682


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## src/main.jsx\n'
sed -n '1,220p' src/main.jsx

printf '\n## VisualizerApp.jsx around the hook call\n'
sed -n '120,170p' src/pages/VisualizerApp.jsx

Repository: ayoub3bidi/bayan-flow

Length of output: 3504


Switch useBodyScrollLock to useLayoutEffect.

This writes layout-affecting body styles when the panel opens. useEffect can let one frame paint before the lock lands, which can cause a scrollbar/layout flash.

Proposed fix
-import { useEffect } from 'react';
+import { useLayoutEffect } from 'react';
@@
 export function useBodyScrollLock(isLocked) {
-  useEffect(() => {
+  useLayoutEffect(() => {
@@
-  }, [isLocked]);
+  }, [isLocked]);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useEffect } from 'react';
/**
* Prevent the document from scrolling while an overlay (side panel, modal) is open.
* Preserves scroll position using position:fixed on body.
*
* @param {boolean} isLocked - Whether scroll should be locked
*/
export function useBodyScrollLock(isLocked) {
useEffect(() => {
if (
!isLocked ||
typeof window === 'undefined' ||
typeof document === 'undefined'
) {
return;
}
const scrollY = window.scrollY;
const { body } = document;
const previousPosition = body.style.position;
const previousTop = body.style.top;
const previousWidth = body.style.width;
const previousOverflow = body.style.overflow;
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.width = '100%';
body.style.overflow = 'hidden';
return () => {
body.style.position = previousPosition;
body.style.top = previousTop;
body.style.width = previousWidth;
body.style.overflow = previousOverflow;
window.scrollTo(0, scrollY);
};
}, [isLocked]);
import { useLayoutEffect } from 'react';
/**
* Prevent the document from scrolling while an overlay (side panel, modal) is open.
* Preserves scroll position using position:fixed on body.
*
* `@param` {boolean} isLocked - Whether scroll should be locked
*/
export function useBodyScrollLock(isLocked) {
useLayoutEffect(() => {
if (
!isLocked ||
typeof window === 'undefined' ||
typeof document === 'undefined'
) {
return;
}
const scrollY = window.scrollY;
const { body } = document;
const previousPosition = body.style.position;
const previousTop = body.style.top;
const previousWidth = body.style.width;
const previousOverflow = body.style.overflow;
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.width = '100%';
body.style.overflow = 'hidden';
return () => {
body.style.position = previousPosition;
body.style.top = previousTop;
body.style.width = previousWidth;
body.style.overflow = previousOverflow;
window.scrollTo(0, scrollY);
};
}, [isLocked]);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useBodyScrollLock.js` around lines 7 - 44, Switch useBodyScrollLock
from useEffect to useLayoutEffect so the body style changes in the
useBodyScrollLock hook are applied before paint and avoid a scrollbar/layout
flash. Update the React import in useBodyScrollLock and keep the existing
lock/unlock logic, including the document/window guards and cleanup that
restores body styles and scroll position.

Comment thread vite.config.js
Comment on lines +112 to +113
const csp = extractCspFromHeadersFile(content);
assertVideoExportCspDirectives(csp, 'dist/_headers');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail the build when dist/_headers is missing.

This validation still has a blind spot: if _headers is not emitted, the hook exits before these checks run. That means the build can pass with no CSP validation at all.

Suggested fix
         closeBundle() {
           const headersPath = path.join(__dirname, 'dist', '_headers');
           if (!fs.existsSync(headersPath)) {
-            return;
+            throw new Error('dist/_headers was not emitted; cannot validate CSP headers');
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const csp = extractCspFromHeadersFile(content);
assertVideoExportCspDirectives(csp, 'dist/_headers');
if (!fs.existsSync(headersPath)) {
throw new Error('dist/_headers was not emitted; cannot validate CSP headers');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vite.config.js` around lines 112 - 113, The CSP validation in the Vite build
hook has a blind spot when dist/_headers is missing, so the build can pass
without running any checks. Update the hook around extractCspFromHeadersFile and
assertVideoExportCspDirectives to explicitly verify that dist/_headers was
emitted before reading it, and fail the build with a clear error if it is
absent.

@github-actions

Copy link
Copy Markdown

Preview for Bayan Flow Staging ready!

Name Link
🔨 Latest commit 3d14525
🔍 Latest deploy log https://github.com/ayoub3bidi/bayan-flow/actions/runs/28291620953
😎 Deploy Preview https://pr-191-bayan-flow-staging.ayoub3bidi.workers.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

Preview alias pr-191 on the staging worker. Updates automatically with new commits.

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.90058% with 79 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/components/OutputConsole.jsx 56.81% 76 Missing ⚠️
src/components/PythonCodePanel.jsx 97.77% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/VisualizerApp.test.jsx (1)

353-367: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the hoisted vi.fn() state before each test. vi.restoreAllMocks() only restores spies; it won’t clear the call history on the module-scoped vi.fn() mocks used here, so the toHaveBeenCalledTimes(...) assertions can leak across cases. Add vi.clearAllMocks() in beforeEach or clear those hoisted mocks explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/VisualizerApp.test.jsx` around lines 353 - 367, Clear the hoisted
vi.fn() mock state before each test in VisualizerApp.test.jsx, because
vi.restoreAllMocks() in afterEach only resets spies and does not wipe call
history on the module-scoped mocks. Update the test setup around
beforeEach/afterEach to call vi.clearAllMocks() (or explicitly reset the hoisted
mocks) so assertions like toHaveBeenCalledTimes stay isolated across cases,
using the existing resetSoundManagerMock, fullScreenMock, and videoExporterMock
setup as the place to keep the test state clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/pages/VisualizerApp.test.jsx`:
- Around line 353-367: Clear the hoisted vi.fn() mock state before each test in
VisualizerApp.test.jsx, because vi.restoreAllMocks() in afterEach only resets
spies and does not wipe call history on the module-scoped mocks. Update the test
setup around beforeEach/afterEach to call vi.clearAllMocks() (or explicitly
reset the hoisted mocks) so assertions like toHaveBeenCalledTimes stay isolated
across cases, using the existing resetSoundManagerMock, fullScreenMock, and
videoExporterMock setup as the place to keep the test state clean.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aed40ac2-5073-41e7-b24f-c592d3c19882

📥 Commits

Reviewing files that changed from the base of the PR and between bcb977c and 3d14525.

📒 Files selected for processing (3)
  • src/pages/VisualizerApp.test.jsx
  • src/test/setup.js
  • src/test/soundManagerMock.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config style Improve styling, design, and animation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant