Skip to content

feat(wallpaper): add Wallhaven proxy route + sectioned picker integration#1902

Open
hognek wants to merge 3 commits into
jaylfc:devfrom
hognek:feat/wallhaven-proxy
Open

feat(wallpaper): add Wallhaven proxy route + sectioned picker integration#1902
hognek wants to merge 3 commits into
jaylfc:devfrom
hognek:feat/wallhaven-proxy

Conversation

@hognek

@hognek hognek commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Rebased onto origin/dev (2026-07-18)
  • Added wallhaven_api_key config field (env-only, never in config.yaml) alongside existing memory_url
  • Created GET /api/wallhaven/search proxy route to Wallhaven API
  • New WallhavenBrowser component: debounced search, thumbnail grid, pagination
  • Re-wired WallpaperPicker to show WallhavenBrowser directly in the Online section of the sectioned picker
  • Config tests: 35/35 passing
  • Route registration verified: /api/wallhaven/search present

Fixes #864

Summary by CodeRabbit

  • New Features
    • Added an online wallpaper browser with debounced search, result previews, pagination, and loading, empty, and error states.
    • Added a collapsible “Browse online” section to the wallpaper picker.
    • Selected online wallpapers can now be applied directly to the active theme.
    • Added secure server-side proxying for wallpaper searches, including optional API-key support and clear upstream error handling.

@hognek
hognek marked this pull request as ready for review July 17, 2026 17:40
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hognek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 178e8315-d71e-4a4e-8994-0303e91d2877

📥 Commits

Reviewing files that changed from the base of the PR and between a682f0e and cc2c1a3.

📒 Files selected for processing (1)
  • desktop/src/components/WallpaperPicker.tsx
📝 Walkthrough

Walkthrough

Adds a Wallhaven API proxy with validation, error handling, and optional API-key forwarding, plus a debounced desktop search browser and wallpaper picker integration for applying selected online images.

Changes

Wallhaven wallpaper browsing

Layer / File(s) Summary
Wallhaven proxy and configuration
tinyagentos/config.py, tinyagentos/routes/wallhaven.py, tinyagentos/routes/__init__.py
Adds environment-only API-key configuration, registers the Wallhaven route, validates search parameters, forwards requests, caps response size, and maps upstream errors.
Proxy behavior coverage
tests/test_wallhaven.py
Tests successful and empty responses, query forwarding, upstream failures, timeouts, and optional API-key headers.
Debounced wallpaper search UI
desktop/src/components/WallhavenBrowser.tsx, desktop/src/components/WallhavenBrowser.test.tsx
Adds debounced searching, idle/loading/error/empty/result states, accessible selection controls, pagination, and component tests.
Online picker integration
desktop/src/components/WallpaperPicker.tsx
Adds a collapsible online browser and updates active-theme wallpaper state after sanitizing and selecting an image URL.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant WallpaperPicker
  participant WallhavenBrowser
  participant WallhavenSearchRoute
  participant WallhavenAPI
  User->>WallpaperPicker: Expand online browsing
  WallpaperPicker->>WallhavenBrowser: Render search panel
  User->>WallhavenBrowser: Enter search query
  WallhavenBrowser->>WallhavenSearchRoute: Request debounced query
  WallhavenSearchRoute->>WallhavenAPI: Forward validated search
  WallhavenAPI-->>WallhavenSearchRoute: Return results
  WallhavenSearchRoute-->>WallhavenBrowser: Return search JSON
  WallhavenBrowser-->>User: Render wallpaper thumbnails
  User->>WallhavenBrowser: Select wallpaper
  WallhavenBrowser->>WallpaperPicker: Pass URL and label
  WallpaperPicker-->>User: Apply wallpaper to active theme
Loading

Suggested reviewers: jaylfc

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The Wallhaven browser/proxy is added, but #864 also requires Settings access, theme-default-first ordering, and custom image upload, which are not implemented. Implement the remaining #864 pieces or narrow the issue scope: add Settings access, theme-default-first ordering, and custom image upload alongside the Wallhaven browser.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Wallhaven proxy support and picker integration.
Out of Scope Changes check ✅ Passed The changes stay focused on Wallhaven browsing, proxying, picker integration, config, and tests with no obvious unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

@gitar-bot

gitar-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

// Apply remote wallpaper directly — set background-image to the full URL
useThemeStore.setState({
wallpaperId: `wallhaven-${Date.now()}`,
wallpaperImage: `url('${url}')`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Unescaped remote URL interpolated into a CSS url('...') value

The selected Wallhaven URL is placed directly inside a CSS string via `url('${url}')`. If a URL ever contains a single quote (e.g. tracked query params such as ?a=b&c='d), the CSS value breaks and the wallpaper silently fails to load. More importantly, arbitrary URL content is flowing unescaped into a style value. Build the url() value with the URL escaped, e.g. `url("${url.replace(/\"/g, '\\"')}")` (or better, set backgroundImage via a JS object style rather than a string), so a stray quote cannot break rendering or be abused.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<WallhavenBrowser
onSelect={(url, label) => {
// Apply remote wallpaper directly — set background-image to the full URL
useThemeStore.setState({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Applying wallpaper via useThemeStore.setState bypasses setWallpaper, leaving state inconsistent

setWallpaper (theme-store.ts:244) records the choice in wallpaperIdByTheme[activeThemeId] and also sets the light/dark/mobile variants (wallpaperLightImage, wallpaperMobileImage, etc.). Calling setState directly sets only wallpaperImage/wallpaperKind/wallpaperOverlayText and a synthetic wallpaperId. Consequences:

  • The pick is never remembered in wallpaperIdByTheme, so switching themes and back drops the selected online wallpaper.
  • wallpaperMobileImage/wallpaperFallback are not updated; on mobile or fallback paths the online wallpaper may not render.
  • wallpaperId (wallhaven-<Date.now()>) is not a tracked wallpaper, so aria-pressed/aria-pressed styling in WallpaperPicker never reflects it.

Consider routing through setWallpaper or adding a dedicated setRemoteWallpaper(url) action that fills all the same fields wallpaperFields does.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if api_key:
headers["X-API-Key"] = api_key

params: dict[str, str | int] = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: categories and purity query params are forwarded to Wallhaven with no validation

These are user-supplied strings passed straight into upstream params. Wallhaven expects a 3-char bitmask (each char 0/1). Invalid values such as categories=999, purity=abc, or an over-long string are sent upstream unvalidated. Wallhaven will reject them, but the proxy should validate the format (and length) before forwarding, returning a 400 rather than proxying a known-bad request. At minimum constrain to /^[01]{1,3}$/ or enforce exactly 3 chars.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/wallhaven.py Outdated
status_code=502,
)

return JSONResponse(resp.json())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: resp.json() is returned directly without error handling

If Wallhaven returns a 200 with a non-JSON or malformed body (rare, but possible on gateway/proxy errors), resp.json() raises and the route returns a 500 with an unhandled traceback instead of the friendly error envelope used elsewhere. Wrap the parse in try/except and return a 502 with a clear message, consistent with the other error branches. Also consider bounding the response size (httpx max_redirects/stream + limits) to avoid proxying an unexpectedly large payload.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/config.py Outdated
config_path=path,
)
# Wallhaven API key is env-only (never written to config.yaml)
import os as _os

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: import os as _os is performed inside the function body

Imports are conventionally placed at module top level. Function-local imports work but are unusual here, add per-call overhead, and diverge from the rest of config.py. Move import os (or from os import environ) to the top of the file alongside the other imports.

Suggested change
import os as _os
cfg.wallhaven_api_key = os.environ.get("WALLHAVEN_API_KEY") or None

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
desktop/src/components/WallpaperPicker.tsx 256 CSS url() escaping is incomplete. Only backslashes and single-quotes are escaped, so a remote URL containing ) (valid in query strings) prematurely terminates url('...') and allows CSS injection / render breakage. Escape/strip ), (, and control characters, and validate the scheme is http(s).
Previous Findings — Resolved (incremental)
  • WallpaperPicker.tsx — Unescaped remote URL in url('...') is now escaped (commit 8f8feb4); prior WARNING resolved (escaping still incomplete — see new WARNING above).
  • WallpaperPicker.tsx — Wallpaper now applied via functional useThemeStore.setState that sets wallpaperIdByTheme and light/mobile/fallback variants; prior SUGGESTION resolved.
  • wallhaven.py / config.py / tests / WallhavenBrowser.tsx — PATCH 2/2 restored the previously-removed hardening (categories/purity/sorting validation, 1 MB cap, resp.json() guard, cancelled check, os import move). Prior SUGGESTION about unvalidated q/sorting resolved.
Files Reviewed (incremental, 2 files with code changes)
  • desktop/src/components/WallpaperPicker.tsx - 1 issue
  • desktop/src/components/WallhavenBrowser.tsx - 0 issues (revert/restore of prior fix)
  • tinyagentos/routes/wallhaven.py - 0 issues (restore)
  • tests/test_wallhaven.py - 0 issues (restore)
  • tinyagentos/config.py - 0 issues (restore)
  • desktop/src/components/WallhavenBrowser.test.tsx - 0 issues (restore)

Fix these issues in Kilo Cloud

Previous Review Summaries (4 snapshots, latest commit a682f0e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit a682f0e)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/routes/wallhaven.py 19 q (and sorting) forwarded to Wallhaven with no validation/length cap; q defaults to empty, allowing unfiltered upstream queries. Make q required with max_length, and cap sorting.
Previous Findings — Resolved

All 5 findings from the prior review (commit 64f0eea4) were verified against the current HEAD (a682f0e4) and are resolved:

  • WallpaperPicker.tsx — remote URL now escaped (url.replace(/\\\\/g, "\\\\\\").replace(/'/g, "\\\'")) before url('...').
  • WallpaperPicker.tsx — wallpaper applied via functional setState setting wallpaperIdByTheme, light/mobile/fallback variants, and uses label as wallpaperOverlayText.
  • wallhaven.pycategories/purity validated against ^[01]{3}$ (400 on mismatch).
  • wallhaven.pyresp.json() guarded with try/except ValueError and response capped at 1 MB.
  • config.pyimport os as _os moved to module top level.
Files Reviewed (7 files)
  • desktop/src/components/WallhavenBrowser.test.tsx
  • desktop/src/components/WallhavenBrowser.tsx
  • desktop/src/components/WallpaperPicker.tsx
  • tests/test_wallhaven.py
  • tinyagentos/config.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/wallhaven.py

Fix these issues in Kilo Cloud

Previous review (commit 64f0eea)

Status: No Issues Found | Recommendation: Merge

All 5 findings from the previous review (commit 15202192e200d605b8ffe0f09cb1be823c76be5c) were resolved in commit 64f0eea4 and verified against the current HEAD. No new issues were introduced in the changed lines.

Resolutions (previously reported, now fixed)

  • WallpaperPicker.tsx: remote URL is now escaped (url.replace(/\\\\/g, "\\\\\\").replace(/'/g, "\\\'")) before being placed in url('...') — fixes CSS injection / render breakage.
  • WallpaperPicker.tsx: wallpaper is now applied through a functional setState that sets wallpaperIdByTheme[activeThemeId], light/mobile/fallback variants, and uses the received label as wallpaperOverlayText.
  • wallhaven.py: categories and purity are validated against ^[01]{3}$ before forwarding (400 on mismatch).
  • wallhaven.py: resp.json() is guarded with try/except ValueError and response size is capped at 1 MB (502 on invalid/oversized).
  • config.py: import os as _os moved to module top level.
Files Reviewed (7 files)
  • desktop/src/components/WallhavenBrowser.test.tsx
  • desktop/src/components/WallhavenBrowser.tsx
  • desktop/src/components/WallpaperPicker.tsx
  • tests/test_wallhaven.py
  • tinyagentos/config.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/wallhaven.py

Previous review (commit 1520219)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
desktop/src/components/WallpaperPicker.tsx 496 Unescaped remote URL interpolated into CSS url('...'); stray quote breaks render / injection risk
tinyagentos/routes/wallhaven.py 842 resp.json() returned directly without error handling; malformed 200 body yields 500 instead of friendly error

SUGGESTION

File Line Issue
desktop/src/components/WallpaperPicker.tsx 494 Direct setState bypasses setWallpaper, leaving wallpaper state inconsistent (no theme memory, no mobile/light variants)
tinyagentos/routes/wallhaven.py 788 categories/purity forwarded to Wallhaven with no format validation
tinyagentos/config.py 748 import os as _os placed inside function body; move to module top
Files Reviewed (7 files)
  • desktop/src/components/WallhavenBrowser.test.tsx
  • desktop/src/components/WallhavenBrowser.tsx
  • desktop/src/components/WallpaperPicker.tsx
  • tests/test_wallhaven.py
  • tinyagentos/config.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/wallhaven.py

Incremental review vs previous commit 2584d1b1: no code changes to the flagged lines were made (PR was rebased onto dev). All 5 prior findings remain valid and unresolved; no new issues introduced in the changed lines.

Fix these issues in Kilo Cloud

Previous review (commit 2584d1b)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
desktop/src/components/WallpaperPicker.tsx 130 Unescaped remote URL interpolated into CSS url('...'); stray quote breaks render / injection risk
tinyagentos/routes/wallhaven.py 71 resp.json() returned directly without error handling; malformed 200 body yields 500 instead of friendly error

SUGGESTION

File Line Issue
desktop/src/components/WallpaperPicker.tsx 128 Direct setState bypasses setWallpaper, leaving wallpaper state inconsistent (no theme memory, no mobile/light variants)
tinyagentos/routes/wallhaven.py 33 categories/purity forwarded to Wallhaven with no format validation
tinyagentos/config.py 176 import os as _os placed inside function body; move to module top
Files Reviewed (7 files)
  • desktop/src/components/WallhavenBrowser.test.tsx
  • desktop/src/components/WallhavenBrowser.tsx
  • desktop/src/components/WallpaperPicker.tsx
  • tests/test_wallhaven.py
  • tinyagentos/config.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/wallhaven.py

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 94.9K · Output: 14K · Cached: 818.2K

@jaylfc

jaylfc commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Thanks — the proxy/security core is clean (no SSRF, auth-gated, API key env-only, 12 tests pass). Two things before merge:

  1. Rebase onto current origin/dev (this branch was cut from 96a33cb6, well behind). config.py conflict is trivial — keep BOTH memory_url and wallhaven_api_key fields.
  2. WallpaperPicker.tsx needs re-wiring, not a merge. dev refactored the picker into sections (getWallpapersBySection + WallpaperTile + SectionHeading + EmptyState) and already scaffolds an "online" section with a "Search Wallhaven" empty state. Please wire WallhavenBrowser into that existing online section rather than re-adding the old standalone Browse-Online button on the flat grid.

Optional hardening (non-blocking): wrap resp.json() in try/except (malformed 200 → 500 today), light per-client rate limit, escape the quote in the url('${url}') CSS interpolation.

@hognek
hognek force-pushed the feat/wallhaven-proxy branch from 2584d1b to 1520219 Compare July 17, 2026 22:43
@hognek hognek changed the title feat(wallpaper): add Wallhaven proxy route + browse-online picker section feat(wallpaper): add Wallhaven proxy route + sectioned picker integration Jul 17, 2026
@jaylfc

jaylfc commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Held on two real (if low-severity) correctness issues plus the Kilo findings. Security core is clean (no SSRF, WALLHAVEN_BASE hardcoded, timeout=15 present, API key env-only), and both of my earlier blocking asks (rebase + sectioned-picker wiring) are folded, thanks. Remaining before merge:

  1. desktop/src/components/WallpaperPicker.tsx (onSelect): the remote URL is interpolated unescaped into CSS url('${url}'); a ' in the URL breaks the render / is a style-injection vector. Escape it.
  2. desktop/src/components/WallpaperPicker.tsx (onSelect): direct useThemeStore.setState bypasses setWallpaper, so the pick is not stored in wallpaperIdByTheme and light/mobile/fallback variants are not set (theme switch loses the wallpaper). Route through setWallpaper.
  3. tinyagentos/routes/wallhaven.py:71: return JSONResponse(resp.json()) is unguarded; a malformed 200 body throws a 500 instead of the friendly 502 envelope. Guard it, and cap the response size.
  4. Nits: categories/purity forwarded with no ^[01]{3}$ validation; import os as _os inside load_config should move to module top; unused label param.

@hognek
hognek force-pushed the feat/wallhaven-proxy branch from 1520219 to 64f0eea Compare July 18, 2026 08:31
@hognek

hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Pushed fixes for the 4 items flagged for merge:

  1. CSS injection: safeUrl with escaped \\ and ' before interpolating into url('...')
  2. Theme persistence: Router through state updater with wallpaperIdByTheme + light/mobile/fallback variants set ✅
  3. JSON guard: try/except ValueError + 1 MB response cap ✅
  4. Nits: categories/purity ^[01]{3}$ validation, import os as _os moved to module top, label used as overlay text ✅

Tests: config 30/30, frontend 22/22, wallhaven targeted 3/3 (test_search_pagination has pre-existing conftest hang — will check CI matrix).

@jaylfc

jaylfc commented Jul 18, 2026

Copy link
Copy Markdown
Owner

The fold looks right, thanks, but the branch is now CONFLICTING against dev (it advanced). Please rebase onto origin/dev and I'll re-review + merge.

@hognek
hognek force-pushed the feat/wallhaven-proxy branch from 64f0eea to a682f0e Compare July 18, 2026 09:21
Comment thread tinyagentos/routes/wallhaven.py Outdated
@router.get("/api/wallhaven/search")
async def wallhaven_search(
request: Request,
q: str = Query(default="", description="Search term"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: q (and sorting) are forwarded to Wallhaven with no validation or length cap.

q defaults to "", so a direct call to this endpoint with no q proxies to Wallhaven's unfiltered "latest uploads" feed, and sorting accepts arbitrary values. Consider making q required (or at least rejecting empty/whitespace) and adding a max length (e.g. max_length=200) to both q and sorting, matching the validation already applied to categories/purity. This keeps the public proxy contract safe from unbounded/abusive upstream queries.

Suggested change
q: str = Query(default="", description="Search term"),
q: str = Query(default="", min_length=1, max_length=200, description="Search term"),

@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: 6

🧹 Nitpick comments (1)
tests/test_wallhaven.py (1)

110-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the new validation and response guards.

Add cases for invalid categories/purity, malformed JSON, and a body exceeding _MAX_RESPONSE_BYTES. These are important defensive branches introduced by this route.

🤖 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 `@tests/test_wallhaven.py` around lines 110 - 186, Add tests for the Wallhaven
search route covering invalid categories or purity parameters, malformed
upstream JSON, and a response body larger than _MAX_RESPONSE_BYTES. Assert each
case returns the expected validation or guarded error response, using the
existing respx setup and client request patterns in the test suite.
🤖 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 `@desktop/src/components/WallhavenBrowser.tsx`:
- Around line 81-85: Update the error-response handling in WallhavenBrowser so
it rechecks request cancellation or staleness after awaiting resp.json() and
before calling setErrorMsg or setStatus. Return without updating state when the
request is obsolete, while preserving the existing error message and status
behavior for the active request.
- Line 43: Initialize the debounceRef useRef in WallhavenBrowser with an
explicit undefined value while retaining its ReturnType<typeof setTimeout> type,
so it satisfies React 19 typings.
- Around line 110-113: WallhavenBrowser currently passes the details-page URL
instead of the direct image asset to WallpaperPicker. In
desktop/src/components/WallhavenBrowser.tsx lines 110-113, update handleSelect
to pass img.path; in desktop/src/components/WallhavenBrowser.test.tsx lines 9-10
and 22-23, swap the fixture url/path values; at lines 166-170, retain the
expectation for the direct image URL.

In `@desktop/src/components/WallpaperPicker.tsx`:
- Around line 181-229: The WallhavenBrowser currently renders in a shrink-0
footer outside the dialog’s scrollable content, causing multi-row results and
pagination to be clipped. Move the “Browse online” toggle and WallhavenBrowser
into the mapped `online` section’s existing overflow-y-auto panel, removing the
duplicate “Search Wallhaven” content or replacing it with this browser while
preserving the existing selection state update behavior.

In `@tinyagentos/config.py`:
- Around line 181-182: The configuration loader must read WALLHAVEN_API_KEY
before the missing-file branch and pass that value into both AppConfig
construction paths; update tinyagentos/config.py lines 181-182 accordingly. In
tests/test_wallhaven.py lines 188-211, set the environment variable before
creating configuration or app state and cover both missing and existing
config-file scenarios without mutating app state to bypass loading.

In `@tinyagentos/routes/wallhaven.py`:
- Around line 55-61: Update the upstream request flow around the AsyncClient
call and response handling to use client.stream(...) with incremental
aiter_bytes() reads. Accumulate and count chunks as they arrive, abort once the
total exceeds _MAX_RESPONSE_BYTES, and preserve the existing response processing
for bodies within the limit; remove reliance on AsyncClient.get() buffering
before the size check.

---

Nitpick comments:
In `@tests/test_wallhaven.py`:
- Around line 110-186: Add tests for the Wallhaven search route covering invalid
categories or purity parameters, malformed upstream JSON, and a response body
larger than _MAX_RESPONSE_BYTES. Assert each case returns the expected
validation or guarded error response, using the existing respx setup and client
request patterns in the test suite.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 72aff574-62a4-4917-b0ec-e5640380a4aa

📥 Commits

Reviewing files that changed from the base of the PR and between 0e6e6fa and a682f0e.

📒 Files selected for processing (7)
  • desktop/src/components/WallhavenBrowser.test.tsx
  • desktop/src/components/WallhavenBrowser.tsx
  • desktop/src/components/WallpaperPicker.tsx
  • tests/test_wallhaven.py
  • tinyagentos/config.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/wallhaven.py

Comment thread desktop/src/components/WallhavenBrowser.tsx Outdated
Comment thread desktop/src/components/WallhavenBrowser.tsx
Comment thread desktop/src/components/WallhavenBrowser.tsx
Comment thread desktop/src/components/WallpaperPicker.tsx Outdated
Comment thread tinyagentos/config.py Outdated
Comment thread tinyagentos/routes/wallhaven.py Outdated
Comment on lines +55 to +61
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{WALLHAVEN_BASE}/search",
params=params,
headers=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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Stream the upstream body to enforce the 1 MB limit.

AsyncClient.get() buffers the complete response before Line 86 checks its size. A large response can therefore consume unbounded memory despite the configured cap. Use client.stream(...), count chunks while reading, and abort once _MAX_RESPONSE_BYTES is exceeded.

#!/bin/bash
# Confirm the declared HTTPX version and locate established streaming patterns.
fd -a -t f '^(pyproject\.toml|uv\.lock|poetry\.lock|requirements.*\.txt)$' . \
  -x sh -c 'echo "=== $1"; rg -n -C2 "\bhttpx\b" "$1"' sh {}

rg -n --type=py -C3 '\.stream\s*\(|aiter_bytes\s*\(' tinyagentos tests

Also applies to: 85-90

🤖 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 `@tinyagentos/routes/wallhaven.py` around lines 55 - 61, Update the upstream
request flow around the AsyncClient call and response handling to use
client.stream(...) with incremental aiter_bytes() reads. Accumulate and count
chunks as they arrive, abort once the total exceeds _MAX_RESPONSE_BYTES, and
preserve the existing response processing for bodies within the limit; remove
reliance on AsyncClient.get() buffering before the size check.

@hognek

hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto origin/dev (e895f71). Resolved conflicts in config.py and WallpaperPicker.tsx. PR is now mergeable — config tests (35/35) pass.

jaylfc pushed a commit that referenced this pull request Jul 18, 2026
…des #1902) (#1982)

* feat(wallpaper): add Wallhaven proxy route + browse-online picker section

- Add wallhaven_api_key config field (env-only, never in repo)
- Create GET /api/wallhaven/search proxy route to wallhaven.cc API
- Keyless by default; optional X-API-Key header when WALLHAVEN_API_KEY set
- Handle rate limiting (429), timeouts (504), and Wallhaven errors (502)
- New WallhavenBrowser component: debounced search, thumbnail grid, pagination
- Integrate WallhavenBrowser into WallpaperPicker as collapsible section
- WallpaperPicker: "Browse online" toggle expands search UI, selecting a
  Wallhaven image applies it as a remote wallpaper
- Backend tests (test_wallhaven.py, 12 tests) with respx mocking
- Frontend tests (WallhavenBrowser.test.tsx, 8 tests; WallpaperPicker 14 tests)

Fixes #864

* fix(wallpaper): escape CSS url, persist wallpaperIdByTheme, guard JSONResponse, validate categories/purity, move os import

- Escape single-quotes and backslashes in remote wallpaper URLs to prevent
  CSS injection and render breakage (WallpaperPicker.tsx onSelect).
- Route through a state updater that sets wallpaperIdByTheme, and include
  light/mobile/fallback variants so theme switch preserves remote wallpapers.
- Use the received label as wallpaperOverlayText instead of discarding it.
- Guard resp.json() with try/except ValueError and cap response at 1 MB
  (wallhaven.py:71).
- Validate categories/purity with ^[01]{3}$ before forwarding to Wallhaven.
- Move import os as _os from inside load_config to module top (config.py).

* fix(wallpaper): bot-fix v2 — Kilo 1S + CodeRabbit 6 findings on Wallhaven

- wallhaven.py: add max_length=200 on q/sorting params, validate sorting
  against known Wallhaven values, use client.stream() + aiter_bytes()
  to enforce 1MB limit incrementally (CR6)
- WallhavenBrowser.tsx: init useRef with undefined (React 19, CR1),
  recheck cancellation after resp.json() (CR2), pass img.path not
  img.url to onSelect (CR3)
- WallpaperPicker.tsx: move WallhavenBrowser into Online section's
  scrollable panel instead of shrink-0 footer (CR4)
- config.py: read WALLHAVEN_API_KEY before missing-file branch so
  fresh installs pick it up (CR5)
- tests: swap url/path fixtures to match real Wallhaven API, add
  sorting validation + max_length + api_key fresh-install tests

* fix(wallpaper): balance JSX closers in WallpaperPicker + add CSRF dependency on wallhaven router

Remove unbalanced </div> that broke the desktop build after CR4 restructure
(WallhavenBrowser moved into Online section). The .map() callback and p-4
wrapper were missing closing tags; add ))} and </div> to properly close both.

Also add missing dependencies=_csrf on the wallhaven router registration in
routes/__init__.py for consistency with every sibling router.

Docs-Reviewed: wallpaper proxy is a protected internal route, no external API docs needed

* fix(wallpaper): use getByRole heading to disambiguate Browse online test query

The 'renders section headings for all four sections' test used getByText('Browse online')
which now matches both the section heading (h4) and the browser-toggle button inside the
Online section. Scope the query to getByRole('heading', ...) so it only matches the h4.

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
…tion

- Add wallhaven_api_key config field (env-only, never in repo)
- Create GET /api/wallhaven/search proxy route to wallhaven.cc API
- Keyless by default; optional X-API-Key header when WALLHAVEN_API_KEY set
- Handle rate limiting (429), timeouts (504), and Wallhaven errors (502)
- New WallhavenBrowser component: debounced search, thumbnail grid, pagination
- Integrate WallhavenBrowser into WallpaperPicker as collapsible section
- WallpaperPicker: "Browse online" toggle expands search UI, selecting a
  Wallhaven image applies it as a remote wallpaper
- Backend tests (test_wallhaven.py, 12 tests) with respx mocking
- Frontend tests (WallhavenBrowser.test.tsx, 8 tests; WallpaperPicker 14 tests)

Fixes jaylfc#864
@hognek
hognek force-pushed the feat/wallhaven-proxy branch from a682f0e to d1d7fb4 Compare July 19, 2026 23:28
…NResponse, validate categories/purity, move os import

- Escape single-quotes and backslashes in remote wallpaper URLs to prevent
  CSS injection and render breakage (WallpaperPicker.tsx onSelect).
- Route through a state updater that sets wallpaperIdByTheme, and include
  light/mobile/fallback variants so theme switch preserves remote wallpapers.
- Use the received label as wallpaperOverlayText instead of discarding it.
- Guard resp.json() with try/except ValueError and cap response at 1 MB
  (wallhaven.py:71).
- Validate categories/purity with ^[01]{3}$ before forwarding to Wallhaven.
- Move import os as _os from inside load_config to module top (config.py).
@hognek
hognek force-pushed the feat/wallhaven-proxy branch from d1d7fb4 to 8f8feb4 Compare July 19, 2026 23:32
onSelect={(url, label) => {
// Escape single-quotes and backslashes to prevent CSS injection
// and render breakage from special characters in remote URLs.
const safeUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: CSS url() escaping is incomplete — still allows injection / render breakage

Only backslashes and single-quotes are escaped. A remote URL containing a literal ) (valid in many query strings, e.g. ?a=b)) prematurely terminates url('...'), and a value like x)'); color:red; // becomes url('x)'); color:red; //') — a real CSS-injection / style-breakage vector. Control characters (\n, \r, \0) are also unescaped. Escape/strip ), (, and whitespace/control chars, and ideally validate the scheme is http(s) before building url('...').

Suggested change
const safeUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const safeUrl = url.replace(/[\\()'\r\n\0]/g, (c) => `\\${c}`);

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

…rol chars, validate http(s) scheme

Kilo WARNING: CSS url() escaping was incomplete. Only backslashes and
single-quotes were escaped, allowing ')' in query strings to prematurely
terminate url('...') and inject CSS. Now also:

- Escape '(' as %28 and ')' as %29
- Strip control characters (\x00-\x1f, \x7f)
- Validate scheme is http(s) before proceeding

Also removes a duplicate 'Browse online' section left behind during
the sectioned-picker rebase — changes are low-risk and build/vitest pass.
@hognek

hognek commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants