fix: gate clipboard access and limit image decode size#6
Conversation
Add a `clipboard_allowed` flag to HostState that defaults to false, blocking guest modules from reading or writing the system clipboard without explicit permission. This prevents silent exfiltration of sensitive clipboard data (passwords, tokens) by malicious modules. Also add a maximum decoded pixel count (4096×4096 = ~16M pixels) for guest-supplied images to prevent image decode bombs where a small compressed image decompresses into a multi-gigabyte RGBA buffer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis pull request introduces interactive per-frame execution and input handling to the Oxide runtime. Changes include a new Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as Browser UI
participant Host as Browser Host
participant Guest as WASM Guest
User->>UI: Move mouse / press key
UI->>Host: capture_input() maps egui events
Host->>Host: Update input_state
loop Per Frame
UI->>Host: tick_frame(dt_ms)
Host->>Guest: live.tick(dt_ms) calls on_frame
Guest->>Host: ui_button / ui_slider / etc. (enqueue WidgetCommand)
Host->>Host: Store WidgetCommand, update widget_states
Guest->>Host: Return from on_frame
Host->>UI: Return from tick_frame
end
UI->>Host: render_canvas() reads widget_commands
Host->>UI: Clone widget_commands and widget_states
UI->>UI: Draw Button/Checkbox/Slider/TextInput widgets
User->>UI: Click widget
UI->>Host: Record click in widget_clicked
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (4)
.gitignore (2)
36-40: Track Firebase project config files in Git.
Line 36(.firebaserc) andLine 39(/firebase.json) are usually shared project/deploy config, not local artifacts. Ignoring them can cause inconsistent deploy targets and harder onboarding.Proposed change
- .firebaserc - /firebase.json + # Keep Firebase project/deploy config versioned + # .firebaserc + # /firebase.json🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore around lines 36 - 40, The .gitignore currently excludes shared Firebase config files (.firebaserc and /firebase.json), which should be tracked; remove those entries from .gitignore so .firebaserc and firebase.json are committed, verify no other ignore patterns still exclude them, and commit the updated .gitignore so project-wide Firebase project/deploy configuration is versioned.
44-46: Do not ignorefunctions/package-lock.jsonfor deterministic builds.Ignoring
Line 46removes lockfile guarantees for dependency resolution in Functions, increasing supply-chain and “works on my machine” drift risk.Proposed change
/functions/node_modules/ /functions/.env -/functions/package-lock.json +# Keep lockfile committed for reproducible installs +# /functions/package-lock.json🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore around lines 44 - 46, The .gitignore currently excludes /functions/package-lock.json which removes the lockfile guarantees; remove the "/functions/package-lock.json" entry from .gitignore so the functions/package-lock.json is tracked and committed, ensuring deterministic dependency resolution and reproducible builds.examples/hello-oxide/src/lib.rs (1)
3-3: Consider usingAtomicI32instead ofstatic mut.
static mutrequires unsafe blocks and is prone to data races. Since this is example code that SDK users may copy, usingAtomicI32would demonstrate safer patterns:♻️ Suggested refactor
-static mut COUNTER: i32 = 0; +use std::sync::atomic::{AtomicI32, Ordering}; +static COUNTER: AtomicI32 = AtomicI32::new(0);Then update the usages:
// Increment COUNTER.fetch_add(1, Ordering::Relaxed); // Reset COUNTER.store(0, Ordering::Relaxed); // Read let count = COUNTER.load(Ordering::Relaxed);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/hello-oxide/src/lib.rs` at line 3, Replace the unsafe global mutable COUNTER with a thread-safe AtomicI32 by changing the declaration of COUNTER from a mutable static to a static AtomicI32 initialized with 0 (use std::sync::atomic::{AtomicI32, Ordering}); then update all uses of COUNTER (e.g., in increment, reset, and read sites) to call COUNTER.fetch_add(1, Ordering::Relaxed), COUNTER.store(0, Ordering::Relaxed), and COUNTER.load(Ordering::Relaxed) respectively so you no longer need unsafe blocks and avoid data races.oxide-landing/script.js (1)
124-143: Consider adding a fallback for environments without IntersectionObserver.The
initScrollAnimations()function relies entirely onIntersectionObserverwithout feature detection. WhileIntersectionObserverhas excellent browser support (Chrome 58+, Firefox 55+, Safari 12.2+, Edge 15+), adding a simple fallback that immediately applies the.visibleclass ensures animated sections remain visible even in unsupported environments.Suggested change
function initScrollAnimations() { const elements = document.querySelectorAll('[data-animate]'); + if (!('IntersectionObserver' in window)) { + elements.forEach((el) => el.classList.add('visible')); + return; + } const observer = new IntersectionObserver((entries) => { entries.forEach(entry => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@oxide-landing/script.js` around lines 124 - 143, The initScrollAnimations function currently assumes IntersectionObserver exists; add feature detection at the top of initScrollAnimations to check window.IntersectionObserver and, if missing, loop over document.querySelectorAll('[data-animate]') and immediately add the 'visible' class (optionally respecting data-delay by using setTimeout with parseInt on dataset.delay) so elements are shown in unsupported environments; otherwise keep the existing IntersectionObserver logic (references: initScrollAnimations, IntersectionObserver, dataset.delay, classList.add('visible')).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/hello-oxide/src/lib.rs`:
- Around line 86-101: The block using canvas_text and ui_text_input is
misformatted and fails cargo fmt; run rustfmt (cargo fmt) and reformat the code
around the ui_text_input call and the subsequent canvas_text branches (look for
ui_text_input, canvas_text, and the format!("Hello, {name}!") usage) so that
spacing, indentation and argument alignment follow rustfmt rules; commit the
formatted file so CI passes.
In `@oxide-browser/src/capabilities.rs`:
- Around line 163-178: The WidgetCommand enum variants are formatted inline and
CI expects multi-line struct-style formatting; update the WidgetCommand variants
(Button, Checkbox, Slider, TextInput) to use multi-line struct formatting (each
field on its own line with trailing commas and braces) and then run cargo fmt
--all to apply consistent formatting; you can leave WidgetValue as-is if already
formatted correctly.
In `@oxide-browser/src/runtime.rs`:
- Around line 163-176: The code block around start_app.call(...) and the
LiveModule construction is misformatted; run rustfmt (cargo fmt) or adjust
spacing so match arms and indentation follow Rust style—ensure the match arm for
Ok(()) and the inner if let Ok(on_frame_fn) = instance.get_typed_func::<u32,
()>(&mut store, "on_frame") { ... } else { ... } are properly indented and the
LiveModule { store, on_frame_fn, } construction is on separate lines consistent
with rustfmt formatting rules.
In `@oxide-browser/src/ui.rs`:
- Around line 7-9: CI reports rustfmt formatting failures in ui.rs; run `cargo
fmt --all` to auto-fix them, then re-check. Ensure the import block containing
ConsoleLevel, DrawCommand, HostState, WidgetCommand, and WidgetValue and nearby
functions/blocks (e.g., any code handling DrawCommand or WidgetValue) are
formatted per rustfmt; if rustfmt cannot auto-fix, adjust
spacing/commas/indentation in those areas to match rustfmt style and re-run the
formatter.
- Around line 151-154: The middle-click branch currently sets
input.mouse_buttons_clicked[2] using i.pointer.middle_down() &&
i.pointer.any_pressed(), which detects a held middle button combined with any
newly-pressed button; replace that expression with the proper click check (use
i.pointer.button_clicked(...) or the appropriate middle click helper) so it
matches the primary/secondary logic (i.pointer.primary_clicked(),
i.pointer.secondary_clicked()) and assigns a true value only when the middle
button was just clicked for input.mouse_buttons_clicked[2].
In `@oxide-landing/index.html`:
- Around line 29-30: The Twitter/X metadata uses content="@oxide_browser" while
visible community/footer links point to "ForgeX_ai", causing inconsistent
account references; update either the meta tags (meta name="twitter:site" and
meta name="twitter:creator") to match the visible anchor hrefs/handles in the
footer/community links or change the footer/community anchor hrefs to use
`@oxide_browser` so the same handle is used across meta tags and visible links
(search for the meta tags and the footer/community anchor elements to locate and
make the handles consistent).
- Around line 84-95: The mobile toggle button lacks an aria-controls attribute
and does not expose its expanded state; add aria-controls="nav-links" to the
button with id "mobile-toggle" and update oxide-landing/script.js to keep the
button's aria-expanded attribute in sync whenever the menu element with id
"nav-links" is opened or closed (toggle aria-expanded="true"/"false" in the same
code that adds/removes the open/closed classes or toggles visibility).
- Around line 395-416: The Analytics initialization currently runs
unconditionally; update the module so that after creating the Firebase app with
initializeApp(firebaseConfig) you first verify user consent (your site’s consent
flag) and call firebase/analytics isSupported() before invoking
getAnalytics(app); only when both consent is true and isSupported() resolves to
true should you call getAnalytics(app), and wrap that call in a try/catch to
swallow/log any errors (refer to initializeApp, getAnalytics and isSupported) so
analytics is not created in unsupported contexts or before consent.
In `@oxide-landing/script.js`:
- Around line 160-173: The catch block that uses the fallback copy path always
shows success even if document.execCommand('copy') fails; change the logic in
that catch to capture the boolean return of document.execCommand('copy') (e.g.,
const success = document.execCommand('copy')), then only apply the success UI
updates on btn (btn.classList.add('copied') and
btn.querySelector('.copy-label').textContent = 'Copied!') when success is true,
and handle the failure case by applying a failure state (e.g., set a different
class or label like 'Copy failed' and remove it after the timeout)—update the
code around range, sel, document.execCommand('copy'), and btn to reflect this
conditional success/failure flow.
In `@oxide-landing/styles.css`:
- Line 24: The CSS custom property --font-sans currently uses an unquoted
BlinkMacSystemFont which triggers the Stylelint value-keyword-case rule; update
the --font-sans value to wrap BlinkMacSystemFont in quotes (so the font name
becomes a string literal) while preserving the rest of the stack ('Inter',
-apple-system, BlinkMacSystemFont, BlinkMacSystemFont, sans-serif → 'Inter',
-apple-system, 'BlinkMacSystemFont', sans-serif) so Stylelint no longer flags
the mixed-case keyword; locate the definition of --font-sans in styles.css and
apply the quoting only to the BlinkMacSystemFont token.
In `@oxide-sdk/src/lib.rs`:
- Around line 215-224: The function signature for _api_ui_slider is misformatted
and failing CI; reformat the declaration to match rustfmt style (run cargo fmt)
or adjust whitespace so the signature aligns with the crate's formatting
conventions; locate the extern declaration for fn _api_ui_slider(...) -> f32 and
apply rustfmt (cargo fmt) to fix indentation, spacing and line breaks so CI
passes.
---
Nitpick comments:
In @.gitignore:
- Around line 36-40: The .gitignore currently excludes shared Firebase config
files (.firebaserc and /firebase.json), which should be tracked; remove those
entries from .gitignore so .firebaserc and firebase.json are committed, verify
no other ignore patterns still exclude them, and commit the updated .gitignore
so project-wide Firebase project/deploy configuration is versioned.
- Around line 44-46: The .gitignore currently excludes
/functions/package-lock.json which removes the lockfile guarantees; remove the
"/functions/package-lock.json" entry from .gitignore so the
functions/package-lock.json is tracked and committed, ensuring deterministic
dependency resolution and reproducible builds.
In `@examples/hello-oxide/src/lib.rs`:
- Line 3: Replace the unsafe global mutable COUNTER with a thread-safe AtomicI32
by changing the declaration of COUNTER from a mutable static to a static
AtomicI32 initialized with 0 (use std::sync::atomic::{AtomicI32, Ordering});
then update all uses of COUNTER (e.g., in increment, reset, and read sites) to
call COUNTER.fetch_add(1, Ordering::Relaxed), COUNTER.store(0,
Ordering::Relaxed), and COUNTER.load(Ordering::Relaxed) respectively so you no
longer need unsafe blocks and avoid data races.
In `@oxide-landing/script.js`:
- Around line 124-143: The initScrollAnimations function currently assumes
IntersectionObserver exists; add feature detection at the top of
initScrollAnimations to check window.IntersectionObserver and, if missing, loop
over document.querySelectorAll('[data-animate]') and immediately add the
'visible' class (optionally respecting data-delay by using setTimeout with
parseInt on dataset.delay) so elements are shown in unsupported environments;
otherwise keep the existing IntersectionObserver logic (references:
initScrollAnimations, IntersectionObserver, dataset.delay,
classList.add('visible')).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 63c4b4d1-7705-460e-8183-c21a9d01c33f
⛔ Files ignored due to path filters (4)
examples/hello-oxide/src/oxide_logo.pngis excluded by!**/*.pngoxide-landing/assets/oxide-screenshot.pngis excluded by!**/*.pngoxide-landing/favicon.icois excluded by!**/*.icooxide-landing/logo.pngis excluded by!**/*.png
📒 Files selected for processing (12)
.gitignoreexamples/hello-oxide/src/lib.rsoxide-browser/src/capabilities.rsoxide-browser/src/runtime.rsoxide-browser/src/ui.rsoxide-landing/index.htmloxide-landing/robots.txtoxide-landing/script.jsoxide-landing/site.webmanifestoxide-landing/sitemap.xmloxide-landing/styles.cssoxide-sdk/src/lib.rs
| canvas_text(20.0, 355.0, 16.0, 180, 140, 255, "Text Input"); | ||
|
|
||
| let name = ui_text_input(30, 20.0, 380.0, 300.0, ""); | ||
| if !name.is_empty() { | ||
| canvas_text( | ||
| 20.0, | ||
| 415.0, | ||
| 18.0, | ||
| 160, | ||
| 220, | ||
| 160, | ||
| &format!("Hello, {name}!"), | ||
| ); | ||
| } else { | ||
| canvas_text(20.0, 415.0, 14.0, 120, 120, 120, "Type your name above..."); | ||
| } |
There was a problem hiding this comment.
Fix formatting to pass CI.
The pipeline reports cargo fmt failure. Run cargo fmt to fix the formatting in this block.
🧰 Tools
🪛 GitHub Actions: CI
[error] 87-87: cargo fmt --all -- --check failed (code not formatted). Diff shows canvas_text call should be formatted onto multiple lines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/hello-oxide/src/lib.rs` around lines 86 - 101, The block using
canvas_text and ui_text_input is misformatted and fails cargo fmt; run rustfmt
(cargo fmt) and reformat the code around the ui_text_input call and the
subsequent canvas_text branches (look for ui_text_input, canvas_text, and the
format!("Hello, {name}!") usage) so that spacing, indentation and argument
alignment follow rustfmt rules; commit the formatted file so CI passes.
| /// A widget the guest wants rendered this frame. | ||
| #[derive(Clone, Debug)] | ||
| pub enum WidgetCommand { | ||
| Button { id: u32, x: f32, y: f32, w: f32, h: f32, label: String }, | ||
| Checkbox { id: u32, x: f32, y: f32, label: String }, | ||
| Slider { id: u32, x: f32, y: f32, w: f32, min: f32, max: f32 }, | ||
| TextInput { id: u32, x: f32, y: f32, w: f32 }, | ||
| } | ||
|
|
||
| /// Persistent widget state maintained by the host across frames. | ||
| #[derive(Clone, Debug)] | ||
| pub enum WidgetValue { | ||
| Bool(bool), | ||
| Float(f32), | ||
| Text(String), | ||
| } |
There was a problem hiding this comment.
Fix formatting to pass CI.
Pipeline reports formatting failures on lines 163-173 (WidgetCommand enum variants should use multi-line struct style). Run cargo fmt --all.
🧰 Tools
🪛 GitHub Actions: CI
[error] 163-173: cargo fmt --all -- --check failed (code not formatted). Diff shows WidgetCommand enum variant formatting should be multi-line struct-style.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-browser/src/capabilities.rs` around lines 163 - 178, The WidgetCommand
enum variants are formatted inline and CI expects multi-line struct-style
formatting; update the WidgetCommand variants (Button, Checkbox, Slider,
TextInput) to use multi-line struct formatting (each field on its own line with
trailing commas and braces) and then run cargo fmt --all to apply consistent
formatting; you can leave WidgetValue as-is if already formatted correctly.
| match start_app.call(&mut store, ()) { | ||
| Ok(()) => Ok(()), | ||
| Ok(()) => { | ||
| // If the guest also exports on_frame, keep the instance alive for the frame loop. | ||
| if let Ok(on_frame_fn) = | ||
| instance.get_typed_func::<u32, ()>(&mut store, "on_frame") | ||
| { | ||
| Ok(Some(LiveModule { | ||
| store, | ||
| on_frame_fn, | ||
| })) | ||
| } else { | ||
| Ok(None) | ||
| } | ||
| } |
There was a problem hiding this comment.
Fix formatting to pass CI.
Pipeline reports formatting issue on line 163. Run cargo fmt to fix.
🧰 Tools
🪛 GitHub Actions: CI
[error] 163-163: cargo fmt --all -- --check failed (code not formatted). Diff shows LiveModule { store, on_frame_fn } formatting.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-browser/src/runtime.rs` around lines 163 - 176, The code block around
start_app.call(...) and the LiveModule construction is misformatted; run rustfmt
(cargo fmt) or adjust spacing so match arms and indentation follow Rust
style—ensure the match arm for Ok(()) and the inner if let Ok(on_frame_fn) =
instance.get_typed_func::<u32, ()>(&mut store, "on_frame") { ... } else { ... }
are properly indented and the LiveModule { store, on_frame_fn, } construction is
on separate lines consistent with rustfmt formatting rules.
| use crate::capabilities::{ | ||
| ConsoleLevel, DrawCommand, HostState, WidgetCommand, WidgetValue, | ||
| }; |
There was a problem hiding this comment.
Multiple formatting issues flagged by CI.
The pipeline reports cargo fmt failures on lines 4, 150, 163, and 577. Run cargo fmt --all to fix these formatting issues.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-browser/src/ui.rs` around lines 7 - 9, CI reports rustfmt formatting
failures in ui.rs; run `cargo fmt --all` to auto-fix them, then re-check. Ensure
the import block containing ConsoleLevel, DrawCommand, HostState, WidgetCommand,
and WidgetValue and nearby functions/blocks (e.g., any code handling DrawCommand
or WidgetValue) are formatted per rustfmt; if rustfmt cannot auto-fix, adjust
spacing/commas/indentation in those areas to match rustfmt style and re-run the
formatter.
| input.mouse_buttons_clicked[0] = i.pointer.primary_clicked(); | ||
| input.mouse_buttons_clicked[1] = i.pointer.secondary_clicked(); | ||
| input.mouse_buttons_clicked[2] = i.pointer.middle_down() | ||
| && i.pointer.any_pressed(); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
egui pointer button_clicked method PointerButton::Middle
💡 Result:
In egui, there is no direct method named button_clicked on widgets or UI elements that takes a PointerButton. Instead, use the Response returned from ui.add or similar methods. PointerButton::Middle represents the middle mouse button (scroll wheel click), as defined in the egui enum: Middle = 2. To detect a middle-click on a widget: 1. Ensure the widget senses clicks (e.g., Button does by default). 2. Capture the Response: let response = ui.button("Test"); 3. Check response.clicked_by(egui::PointerButton::Middle) This returns true if the widget was clicked this frame specifically by the middle button. It ignores accessibility clicks or keyboard equivalents; for primary clicks including those, use response.clicked. Example: let response = ui.button("Middle-click me"); if response.clicked_by(egui::PointerButton::Middle) { println!("Middle button clicked!"); } clicked_by checks if the response sensed a click AND the input state reports pointer.button_clicked(button) for that frame. For global input (not widget-specific), use ctx.input(|i| i.pointer.button_clicked(egui::PointerButton::Middle)). Source: egui latest docs.rs (0.28+ as of 2026).
Citations:
- 1: https://docs.rs/egui/latest/egui/enum.PointerButton.html
- 2: https://docs.rs/egui/latest/egui/struct.PointerState.html
- 3: https://docs.rs/egui/latest/egui/response/struct.Response.html
🏁 Script executed:
# Find and read Cargo.toml to check egui version
find . -name "Cargo.toml" -type f | head -5Repository: niklabh/oxide
Length of output: 223
🏁 Script executed:
# Read the Cargo.toml files to find egui dependency
fd "Cargo.toml" -x cat {} \; 2>/dev/null | grep -A 2 -B 2 "egui" | head -20Repository: niklabh/oxide
Length of output: 209
🏁 Script executed:
# Check the actual code in ui.rs around lines 138-184
fd "ui.rs" -path "*/src/*" | head -1Repository: niklabh/oxide
Length of output: 225
🏁 Script executed:
# Locate ui.rs in oxide-browser
find . -path "*/oxide-browser/src/ui.rs" -type fRepository: niklabh/oxide
Length of output: 82
🏁 Script executed:
# Read the oxide-browser Cargo.toml to confirm egui version
cat oxide-browser/Cargo.toml | grep -A 2 -B 2 "egui"Repository: niklabh/oxide
Length of output: 209
🏁 Script executed:
# Read the ui.rs file around lines 138-184
sed -n '138,184p' oxide-browser/src/ui.rsRepository: niklabh/oxide
Length of output: 1839
🏁 Script executed:
# Also check if there are any imports or references to button_clicked in the file
grep -n "button_clicked\|middle_clicked\|middle_down\|any_pressed" oxide-browser/src/ui.rsRepository: niklabh/oxide
Length of output: 248
🏁 Script executed:
# Let's verify button_clicked method exists in egui 0.31 documentation
# by checking if there are any usages in the codebase
grep -rn "button_clicked" oxide-browser/Repository: niklabh/oxide
Length of output: 131
🏁 Script executed:
# Also check what methods are available on PointerState in the egui 0.31 docs
# by looking at imports and how pointer is used elsewhere
grep -n "i\.pointer\." oxide-browser/src/ui.rs | head -15Repository: niklabh/oxide
Length of output: 611
🏁 Script executed:
# Let's check egui 0.31 specifically to confirm button_clicked availability
# by looking at what methods are used on pointer in other files
grep -rn "pointer\." oxide-browser/src/ | grep -E "primary_clicked|secondary_clicked|button_" | head -10Repository: niklabh/oxide
Length of output: 262
🏁 Script executed:
# Let's create a simple Python script to verify the logic issue
# and understand what the current code does vs what it should do
python3 << 'EOF'
# Analyzing the logic of middle click detection
print("=== Current Logic Analysis ===")
print("Current: middle_down() && any_pressed()")
print("")
print("This fires when:")
print(" - Middle button is currently held down AND")
print(" - ANY button was just pressed in this frame")
print("")
print("Problem: This doesn't detect a middle CLICK (press then release)")
print("Example: If user clicks middle, it only fires if another button is also pressed")
print("")
print("=== Correct Logic ===")
print("Correct: button_clicked(PointerButton::Middle)")
print("")
print("This fires when:")
print(" - Middle button was pressed this frame")
print(" - Regardless of other buttons")
print("")
print("This is the standard way to detect a single click event")
EOFRepository: niklabh/oxide
Length of output: 600
🏁 Script executed:
# Check if there's any documentation or comments about egui methods in the codebase
grep -rn "egui::" oxide-browser/src/ui.rs | head -20Repository: niklabh/oxide
Length of output: 1447
Middle click detection logic is incorrect.
The current implementation middle_down() && any_pressed() detects when the middle button is held down AND any button was just pressed—not a middle click event. For consistency with the primary and secondary click detection on lines 151-152, use button_clicked() instead:
Fix
input.mouse_buttons_clicked[0] = i.pointer.primary_clicked();
input.mouse_buttons_clicked[1] = i.pointer.secondary_clicked();
- input.mouse_buttons_clicked[2] = i.pointer.middle_down()
- && i.pointer.any_pressed();
+ input.mouse_buttons_clicked[2] = i.pointer.button_clicked(egui::PointerButton::Middle);📝 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.
| input.mouse_buttons_clicked[0] = i.pointer.primary_clicked(); | |
| input.mouse_buttons_clicked[1] = i.pointer.secondary_clicked(); | |
| input.mouse_buttons_clicked[2] = i.pointer.middle_down() | |
| && i.pointer.any_pressed(); | |
| input.mouse_buttons_clicked[0] = i.pointer.primary_clicked(); | |
| input.mouse_buttons_clicked[1] = i.pointer.secondary_clicked(); | |
| input.mouse_buttons_clicked[2] = i.pointer.button_clicked(egui::PointerButton::Middle); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-browser/src/ui.rs` around lines 151 - 154, The middle-click branch
currently sets input.mouse_buttons_clicked[2] using i.pointer.middle_down() &&
i.pointer.any_pressed(), which detects a held middle button combined with any
newly-pressed button; replace that expression with the proper click check (use
i.pointer.button_clicked(...) or the appropriate middle click helper) so it
matches the primary/secondary logic (i.pointer.primary_clicked(),
i.pointer.secondary_clicked()) and assigns a true value only when the middle
button was just clicked for input.mouse_buttons_clicked[2].
| <div class="nav-links" id="nav-links"> | ||
| <a href="#about">About</a> | ||
| <a href="#features">Features</a> | ||
| <a href="#token">Token</a> | ||
| <a href="#community">Community</a> | ||
| <a href="https://github.com/niklabh/oxide" target="_blank" rel="noopener">GitHub</a> | ||
| </div> | ||
| <div class="nav-actions"> | ||
| <a href="https://pump.fun/coin/F9GohhDUsdcPCnARhrdrU68erD8ohfcW8pZZrjNPjHBQ" target="_blank" rel="noopener" class="btn btn-primary btn-sm">Buy $OXIDE</a> | ||
| <button class="mobile-toggle" id="mobile-toggle" aria-label="Toggle menu"> | ||
| <span></span><span></span><span></span> | ||
| </button> |
There was a problem hiding this comment.
Expose the mobile nav state to assistive tech.
The toggle button never indicates which menu it controls or whether it's open. Add aria-controls="nav-links" in the markup and keep aria-expanded synced from oxide-landing/script.js.
Suggested change
- <button class="mobile-toggle" id="mobile-toggle" aria-label="Toggle menu">
+ <button class="mobile-toggle" id="mobile-toggle" aria-label="Toggle menu" aria-controls="nav-links" aria-expanded="false">
<span></span><span></span><span></span>
</button> toggle.addEventListener('click', () => {
links.classList.toggle('open');
toggle.classList.toggle('active');
+ toggle.setAttribute('aria-expanded', links.classList.contains('open') ? 'true' : 'false');
});
links.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
links.classList.remove('open');
toggle.classList.remove('active');
+ toggle.setAttribute('aria-expanded', 'false');
});
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-landing/index.html` around lines 84 - 95, The mobile toggle button
lacks an aria-controls attribute and does not expose its expanded state; add
aria-controls="nav-links" to the button with id "mobile-toggle" and update
oxide-landing/script.js to keep the button's aria-expanded attribute in sync
whenever the menu element with id "nav-links" is opened or closed (toggle
aria-expanded="true"/"false" in the same code that adds/removes the open/closed
classes or toggles visibility).
| <script type="module"> | ||
| // Import the functions you need from the SDKs you need | ||
| import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.js"; | ||
| import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js"; | ||
| // TODO: Add SDKs for Firebase products that you want to use | ||
| // https://firebase.google.com/docs/web/setup#available-libraries | ||
|
|
||
| // Your web app's Firebase configuration | ||
| // For Firebase JS SDK v7.20.0 and later, measurementId is optional | ||
| const firebaseConfig = { | ||
| apiKey: "AIzaSyCuHF0T-CBrlLp3Drr8AnKKyQlLQw_vHvA", | ||
| authDomain: "oxide-2c1e9.firebaseapp.com", | ||
| projectId: "oxide-2c1e9", | ||
| storageBucket: "oxide-2c1e9.firebasestorage.app", | ||
| messagingSenderId: "157641973768", | ||
| appId: "1:157641973768:web:da7e3f879c1041aa2471bd", | ||
| measurementId: "G-MKN5CMDE7L" | ||
| }; | ||
|
|
||
| // Initialize Firebase | ||
| const app = initializeApp(firebaseConfig); | ||
| const analytics = getAnalytics(app); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the oxide-landing/index.html file
find . -name "index.html" -path "*/oxide-landing/*" -type fRepository: niklabh/oxide
Length of output: 83
🏁 Script executed:
# Also search for any Firebase-related files to understand the context better
fd -t f "index.html" | head -20Repository: niklabh/oxide
Length of output: 81
🏁 Script executed:
# Check for any consent-related code in the repository
rg -i "analytics.*consent|consent.*analytics" --type html --type jsRepository: niklabh/oxide
Length of output: 39
🌐 Web query:
Firebase JavaScript SDK isSupported analytics API documentation
💡 Result:
The Firebase JavaScript SDK provides an isSupported function for Analytics to check if the current browser environment supports Firebase Analytics initialization. This is a static function exported from the analytics module. Signature (Modular SDK v9+): export declare function isSupported: Promise; It performs four checks: 1. Not a browser extension environment. 2. Cookies are enabled. 3. IndexedDB is supported. 4. Current browser context is valid for IndexedDB.open. Usage example (inferred from docs): import { isSupported } from 'firebase/analytics'; const analyticsSupported = await isSupported; if (analyticsSupported) { // Initialize and use analytics const analytics = getAnalytics(app); } This API exists in both v8 namespace SDK (firebase.analytics.isSupported) and modular SDK (@firebase/analytics). Note: Analytics does not work in Node.js environments. Official docs confirm support in modern browsers like Chrome, Firefox, Safari, Edge. The modularfirebase.web.app site is an unmaintained EAP reference pointing to official docs, but provides the clearest description.
Citations:
- 1: https://modularfirebase.web.app/reference/analytics.issupported
- 2: https://firebase.google.com/docs/reference/js/v8/firebase.analytics
- 3: https://firebase.google.com/docs/reference/js/analytics
- 4: https://modularfirebase.web.app/reference/analytics
- 5: https://www.jsdocs.io/package/@firebase/analytics
🏁 Script executed:
# Read the actual file to verify the code at lines 395-416
sed -n '395,416p' oxide-landing/index.htmlRepository: niklabh/oxide
Length of output: 1164
🏁 Script executed:
# Check surrounding lines for context
sed -n '390,420p' oxide-landing/index.htmlRepository: niklabh/oxide
Length of output: 1281
🏁 Script executed:
# Search for any consent or GDPR handling in the entire HTML file
rg -i "consent|gdpr|cookie.*policy|localStorage" oxide-landing/index.htmlRepository: niklabh/oxide
Length of output: 39
Add consent check and isSupported() guard before initializing Firebase Analytics.
The code initializes Analytics unconditionally on every page load, which creates two problems: (1) getAnalytics() can throw in unsupported contexts like browser extensions or environments without IndexedDB support, and (2) tracking begins before any user consent is obtained on a public landing page, creating privacy and compliance risks.
Check isSupported() and verify user consent before initialization:
Suggested change
- import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js";
+ import { getAnalytics, isSupported } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js";
@@
// Initialize Firebase
const app = initializeApp(firebaseConfig);
- const analytics = getAnalytics(app);
+ const hasAnalyticsConsent = window.localStorage.getItem('analytics-consent') === 'granted';
+ if (hasAnalyticsConsent) {
+ isSupported().then((supported) => {
+ if (supported) {
+ getAnalytics(app);
+ }
+ });
+ }📝 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.
| <script type="module"> | |
| // Import the functions you need from the SDKs you need | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.js"; | |
| import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js"; | |
| // TODO: Add SDKs for Firebase products that you want to use | |
| // https://firebase.google.com/docs/web/setup#available-libraries | |
| // Your web app's Firebase configuration | |
| // For Firebase JS SDK v7.20.0 and later, measurementId is optional | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyCuHF0T-CBrlLp3Drr8AnKKyQlLQw_vHvA", | |
| authDomain: "oxide-2c1e9.firebaseapp.com", | |
| projectId: "oxide-2c1e9", | |
| storageBucket: "oxide-2c1e9.firebasestorage.app", | |
| messagingSenderId: "157641973768", | |
| appId: "1:157641973768:web:da7e3f879c1041aa2471bd", | |
| measurementId: "G-MKN5CMDE7L" | |
| }; | |
| // Initialize Firebase | |
| const app = initializeApp(firebaseConfig); | |
| const analytics = getAnalytics(app); | |
| <script type="module"> | |
| // Import the functions you need from the SDKs you need | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.js"; | |
| import { getAnalytics, isSupported } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js"; | |
| // TODO: Add SDKs for Firebase products that you want to use | |
| // https://firebase.google.com/docs/web/setup#available-libraries | |
| // Your web app's Firebase configuration | |
| // For Firebase JS SDK v7.20.0 and later, measurementId is optional | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyCuHF0T-CBrlLp3Drr8AnKKyQlLQw_vHvA", | |
| authDomain: "oxide-2c1e9.firebaseapp.com", | |
| projectId: "oxide-2c1e9", | |
| storageBucket: "oxide-2c1e9.firebasestorage.app", | |
| messagingSenderId: "157641973768", | |
| appId: "1:157641973768:web:da7e3f879c1041aa2471bd", | |
| measurementId: "G-MKN5CMDE7L" | |
| }; | |
| // Initialize Firebase | |
| const app = initializeApp(firebaseConfig); | |
| const hasAnalyticsConsent = window.localStorage.getItem('analytics-consent') === 'granted'; | |
| if (hasAnalyticsConsent) { | |
| isSupported().then((supported) => { | |
| if (supported) { | |
| getAnalytics(app); | |
| } | |
| }); | |
| } |
🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 405-405: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.
(gcp-api-key)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-landing/index.html` around lines 395 - 416, The Analytics
initialization currently runs unconditionally; update the module so that after
creating the Firebase app with initializeApp(firebaseConfig) you first verify
user consent (your site’s consent flag) and call firebase/analytics
isSupported() before invoking getAnalytics(app); only when both consent is true
and isSupported() resolves to true should you call getAnalytics(app), and wrap
that call in a try/catch to swallow/log any errors (refer to initializeApp,
getAnalytics and isSupported) so analytics is not created in unsupported
contexts or before consent.
| } catch { | ||
| const range = document.createRange(); | ||
| range.selectNodeContents(text); | ||
| const sel = window.getSelection(); | ||
| sel.removeAllRanges(); | ||
| sel.addRange(range); | ||
| document.execCommand('copy'); | ||
| sel.removeAllRanges(); | ||
| btn.classList.add('copied'); | ||
| btn.querySelector('.copy-label').textContent = 'Copied!'; | ||
| setTimeout(() => { | ||
| btn.classList.remove('copied'); | ||
| btn.querySelector('.copy-label').textContent = 'Copy'; | ||
| }, 2000); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "script.js" | grep -E "oxide-landing|script"Repository: niklabh/oxide
Length of output: 80
🏁 Script executed:
cat -n oxide-landing/script.js | sed -n '140,185p'Repository: niklabh/oxide
Length of output: 1956
🌐 Web query:
document.execCommand('copy') return value false behavior
💡 Result:
document.execCommand('copy') returns false when the command is unsupported, disabled, or cannot be executed successfully. For the 'copy' command specifically, this typically occurs if: - It is not invoked as part of a trusted user interaction (e.g., directly from console, setTimeout, or synthetic events). Browsers require a user gesture like a click for security reasons to prevent unauthorized clipboard access. - There is no valid selection to copy (e.g., no text selected in a focusable element like textarea/input). - The document lacks focus, or the target element cannot be focused/selected properly (e.g., hidden elements, overlays blocking focus). - Browser-specific issues, such as improper range selection (e.g., selectNodeContents vs selectNode on textarea in Chrome). Note: execCommand is deprecated; use the modern Clipboard API (navigator.clipboard.writeText) where possible, which throws errors instead of returning false on failure. To handle: Check the return value and provide fallback UX, e.g., manual copy instructions if false.
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
- 2: https://stackoverflow.com/questions/33321095/cannot-use-document-execcommandcopy-from-developer-console
- 3: https://stackoverflow.com/questions/48409145/why-document-execcommandcopy-working-in-console-but-not-working-in-code
- 4: https://stackoverflow.com/questions/35719922/find-out-why-querycommandenabled-returns-false
- 5: https://stackoverflow.com/questions/47879184/document-execcommandcopy-not-working-on-chrome
Only show "Copied!" when the fallback actually copies.
document.execCommand('copy') can return false when the command fails (e.g., no valid selection, browser restrictions, or browser-specific issues). The catch block always flips the UI to success, showing users the address was copied when it actually wasn't. Check the return value and surface a failure state instead.
Suggested change
- document.execCommand('copy');
- sel.removeAllRanges();
- btn.classList.add('copied');
- btn.querySelector('.copy-label').textContent = 'Copied!';
- setTimeout(() => {
- btn.classList.remove('copied');
- btn.querySelector('.copy-label').textContent = 'Copy';
- }, 2000);
+ const copied = typeof document.execCommand === 'function' && document.execCommand('copy');
+ sel.removeAllRanges();
+ if (!copied) {
+ btn.querySelector('.copy-label').textContent = 'Copy failed';
+ setTimeout(() => {
+ btn.querySelector('.copy-label').textContent = 'Copy';
+ }, 2000);
+ return;
+ }
+ btn.classList.add('copied');
+ btn.querySelector('.copy-label').textContent = 'Copied!';
+ setTimeout(() => {
+ btn.classList.remove('copied');
+ btn.querySelector('.copy-label').textContent = 'Copy';
+ }, 2000);📝 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.
| } catch { | |
| const range = document.createRange(); | |
| range.selectNodeContents(text); | |
| const sel = window.getSelection(); | |
| sel.removeAllRanges(); | |
| sel.addRange(range); | |
| document.execCommand('copy'); | |
| sel.removeAllRanges(); | |
| btn.classList.add('copied'); | |
| btn.querySelector('.copy-label').textContent = 'Copied!'; | |
| setTimeout(() => { | |
| btn.classList.remove('copied'); | |
| btn.querySelector('.copy-label').textContent = 'Copy'; | |
| }, 2000); | |
| } catch { | |
| const range = document.createRange(); | |
| range.selectNodeContents(text); | |
| const sel = window.getSelection(); | |
| sel.removeAllRanges(); | |
| sel.addRange(range); | |
| const copied = typeof document.execCommand === 'function' && document.execCommand('copy'); | |
| sel.removeAllRanges(); | |
| if (!copied) { | |
| btn.querySelector('.copy-label').textContent = 'Copy failed'; | |
| setTimeout(() => { | |
| btn.querySelector('.copy-label').textContent = 'Copy'; | |
| }, 2000); | |
| return; | |
| } | |
| btn.classList.add('copied'); | |
| btn.querySelector('.copy-label').textContent = 'Copied!'; | |
| setTimeout(() => { | |
| btn.classList.remove('copied'); | |
| btn.querySelector('.copy-label').textContent = 'Copy'; | |
| }, 2000); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-landing/script.js` around lines 160 - 173, The catch block that uses
the fallback copy path always shows success even if document.execCommand('copy')
fails; change the logic in that catch to capture the boolean return of
document.execCommand('copy') (e.g., const success =
document.execCommand('copy')), then only apply the success UI updates on btn
(btn.classList.add('copied') and btn.querySelector('.copy-label').textContent =
'Copied!') when success is true, and handle the failure case by applying a
failure state (e.g., set a different class or label like 'Copy failed' and
remove it after the timeout)—update the code around range, sel,
document.execCommand('copy'), and btn to reflect this conditional
success/failure flow.
| --radius-md: 12px; | ||
| --radius-lg: 20px; | ||
| --radius-xl: 24px; | ||
| --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "styles.css" | head -20Repository: niklabh/oxide
Length of output: 83
🏁 Script executed:
find . -type f \( -name ".stylelintrc*" -o -name "stylelint.config.*" \) | head -10Repository: niklabh/oxide
Length of output: 76
🏁 Script executed:
cat package.json | grep -A 20 -B 5 "stylelint" || echo "No stylelint config in package.json root"Repository: niklabh/oxide
Length of output: 159
🏁 Script executed:
git ls-files oxide-landing/ | head -20Repository: niklabh/oxide
Length of output: 303
🏁 Script executed:
cat .stylelintrc.jsonRepository: niklabh/oxide
Length of output: 1063
🏁 Script executed:
cat oxide-landing/styles.css | head -30Repository: niklabh/oxide
Length of output: 1025
🏁 Script executed:
web_search
Stylelint value-keyword-case rule behavior with system font names like BlinkMacSystemFontRepository: niklabh/oxide
Length of output: 155
🏁 Script executed:
grep -n "BlinkMacSystemFont\|stylelint" oxide-landing/styles.cssRepository: niklabh/oxide
Length of output: 132
🏁 Script executed:
cat .stylelintrc.json | jq .rulesRepository: niklabh/oxide
Length of output: 943
🌐 Web query:
Stylelint value-keyword-case rule system fonts BlinkMacSystemFont
💡 Result:
The Stylelint rule value-keyword-case enforces lowercase (or uppercase) casing for CSS keyword values by default. System fonts like BlinkMacSystemFont use camelCase, which triggers a violation when "value-keyword-case": "lower" is enabled. BlinkMacSystemFont is a vendor-prefixed system font name for Chromium-based browsers (e.g., Chrome). It is case-insensitive in CSS but conventionally written in camelCase. Stylelint treats it as a keyword value and flags non-lowercase casing unless configured otherwise. To handle this: - Use the ignoreKeywords secondary option to exempt specific system fonts: "value-keyword-case": ["lower", { "ignoreKeywords": ["BlinkMacSystemFont", "-apple-system"] }] - Alternatively, ignore entire properties like font-family: "value-keyword-case": ["lower", { "ignoreProperties": ["font-family", "font"] }] - The rule ignores custom-idents (like font family names) for known properties in some cases, but system font keywords may still be checked unless ignored. This is a common issue discussed in Stylelint GitHub issues for font-family values containing keywords or camelCase system fonts. No automatic exemption exists for all system fonts; explicit configuration is required. Official docs confirm options like ignoreKeywords (array or regex), ignoreProperties, ignoreFunctions, and camelCaseSvgKeywords (for SVG keywords like currentColor, default false).
Citations:
- 1: https://stylelint.io/user-guide/rules/value-keyword-case/
- 2: False positive of
font-familySCSS variables forvalue-keyword-casestylelint/stylelint#4622 - 3: Add camelCaseSvgKeywords to value-keyword-case and set default to false stylelint/stylelint#4884
- 4: value-keyword-case for currentColor in non SVG CSS is breaking code stylelint/stylelint#5863
Quote BlinkMacSystemFont to fix the Stylelint value-keyword-case violation.
The value-keyword-case rule is enabled by default in the extended config and flags mixed-case keywords like BlinkMacSystemFont. Quoting the font name converts it to a string literal, which exempts it from the keyword case enforcement.
Suggested change
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+ --font-sans: 'Inter', -apple-system, 'BlinkMacSystemFont', sans-serif;📝 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.
| --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| --font-sans: 'Inter', -apple-system, 'BlinkMacSystemFont', sans-serif; |
🧰 Tools
🪛 Stylelint (17.4.0)
[error] 24-24: Expected "BlinkMacSystemFont" to be "blinkmacsystemfont" (value-keyword-case)
(value-keyword-case)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-landing/styles.css` at line 24, The CSS custom property --font-sans
currently uses an unquoted BlinkMacSystemFont which triggers the Stylelint
value-keyword-case rule; update the --font-sans value to wrap BlinkMacSystemFont
in quotes (so the font name becomes a string literal) while preserving the rest
of the stack ('Inter', -apple-system, BlinkMacSystemFont, BlinkMacSystemFont,
sans-serif → 'Inter', -apple-system, 'BlinkMacSystemFont', sans-serif) so
Stylelint no longer flags the mixed-case keyword; locate the definition of
--font-sans in styles.css and apply the quoting only to the BlinkMacSystemFont
token.
| #[link_name = "api_ui_slider"] | ||
| fn _api_ui_slider( | ||
| id: u32, | ||
| x: f32, | ||
| y: f32, | ||
| w: f32, | ||
| min: f32, | ||
| max: f32, | ||
| initial: f32, | ||
| ) -> f32; |
There was a problem hiding this comment.
Fix formatting to pass CI.
Pipeline reports formatting issue on line 213 (_api_ui_slider signature). Run cargo fmt.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@oxide-sdk/src/lib.rs` around lines 215 - 224, The function signature for
_api_ui_slider is misformatted and failing CI; reformat the declaration to match
rustfmt style (run cargo fmt) or adjust whitespace so the signature aligns with
the crate's formatting conventions; locate the extern declaration for fn
_api_ui_slider(...) -> f32 and apply rustfmt (cargo fmt) to fix indentation,
spacing and line breaks so CI passes.
Summary
clipboard_allowedflag toHostState(defaults tofalse)api_clipboard_readandapi_clipboard_writenow check the flag and log a warning when blockedapi_canvas_imageto prevent image decompression bombsTest plan
clipboard_allowed = trueenables clipboard access🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Chores