Skip to content

feat(browser): frontend chrome rebuild — multi-window + tab model (PR 4/10)#303

Merged
jaylfc merged 15 commits into
masterfrom
feat/browser-pr-4-frontend-chrome
May 4, 2026
Merged

feat(browser): frontend chrome rebuild — multi-window + tab model (PR 4/10)#303
jaylfc merged 15 commits into
masterfrom
feat/browser-pr-4-frontend-chrome

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented May 4, 2026

Fourth of ten PRs implementing BrowserApp v2 per the design doc.

First user-visible UI deliverable of BrowserApp v2. Replaces the 503-line BrowserApp.tsx monolith with a multi-window, multi-tab browser shell using the compact unified URL bar (Q8 Layout A — Safari 15+ style).

Summary

Backend

  • windows.py/api/desktop/browser/windows GET/PUT/DELETE for browser-window state persistence
  • suggest.py/api/desktop/browser/suggest local-only address-bar autocomplete (history + bookmarks per profile, no online suggest API)
  • BrowserStore extended: upsert_window / list_windows / delete_window / add_history / search_history / add_bookmark / list_bookmarks. All multi-user keyed.
  • Legacy tinyagentos/routes/desktop.py:browser_proxy + _rewrite_html_urls deleted (PR 3's /api/desktop/browser/proxy is the only proxy now)

Frontend store

  • browser-store Zustand — one entry per window: tabs, activeTab, profile, recentlyClosed (max 50), discard state
  • Actions: createWindow, removeWindow, addTab, closeTab, setActiveTab, pinTab, unpinTab, navigateTab, goBack, goForward, markTabDiscarded, markTabLive, setTabZoom, moveTab

Frontend chrome (compact unified bar — Q8 Layout A)

  • Chrome.tsx — back/forward/refresh + profile chip (display only; PR 5 wires the dropdown)
  • TabStrip.tsx — pinned tabs (favicon-only) + inactive tabs (favicon + title) + active tab (wider, hosts AddressBar in PR 5+)
  • AddressBar.tsx + AddressSuggest.tsx — URL input with separate input value vs tab URL, Enter commits, Esc reverts, debounced 150ms suggest popover with keyboard nav
  • TabRenderer.tsx — iframe pool with display:none switching for live tabs + snapshot card for discarded tabs + 60s discard scheduler (10-min idle, hard cap 12 live tabs)
  • BrowserApp/BrowserApp.tsx — top-level container; auto-creates window in store on mount, idempotent

Mobile (Q6 — single instance + window chooser)

  • useIsMobile(600) switches layout
  • TabOverview.tsx — full-screen grid of tab cards (Pinned section + Open section + New tab button)
  • WindowChooser.tsx — sheet listing all browser windows for the user with current marker
  • Bottom URL bar with Tabs + Windows buttons

Cross-cutting

  • FindInPage.tsx — Cmd+F overlay, broadcasts to iframe via postMessage (copilot.js will respond in PR 6)
  • keyboard.ts — Cmd+T (new tab), Cmd+W (close), Cmd+L (focus address), Cmd+F (find), Cmd+0/+/- (zoom)
  • Per-tab zoom via CSS transform, clamped [0.5, 3.0]
  • MoveTabMenu.tsx — right-click move-to-window (DOM-portal drag-and-drop with iframe state preservation deferred per plan)
  • Persistence: useSessionPersistence extended for browser windows (debounced 2s save, mirrors existing pattern)
  • BrowserApp unmount cleanup: removes window from browser-store + calls backend DELETE

What this does not land

  • Profile switching dropdown (chip is display-only) — PR 5
  • Reader mode — PR 5
  • Live exclusion (audio/video/form/upload) for discard — PR 5
  • Configurable discard timeout in Settings — PR 5
  • Agent presence pill / co-pilot mode — PR 6
  • Native HTML5 drag-and-drop with DOM-portal iframe state preservation — explicit deferral; right-click "Move tab to window" is the PR 4 affordance

Test Plan

  • pytest tests/routes/desktop_browser/test_windows.py -v — 6 tests
  • pytest tests/routes/desktop_browser/test_suggest.py -v — 8 tests
  • Full backend pytest tests/routes/desktop_browser/ — 143 tests, all green
  • npx vitest run381 frontend tests, all green
  • Broader regression pytest tests/routes/ tests/test_secrets.py — 535 tests, no regressions

Spec

docs/superpowers/specs/2026-05-03-browser-app-v2-design.md §5 (frontend), Q6 (mobile), Q8 Layout A (compact chrome).

Cumulative shipping arc:

After PR 1:  cookie-aware storage backend ready
After PR 2:  proxy endpoint exists with auth + SSRF gate
After PR 3:  full lxml rewriter + cookie-aware HTTP fetch
After PR 4:  new compact chrome, multi-window, tab model (this PR)   (NEW SHELL)
After PR 5:  profile switching + Reader mode + live-exclusion + Settings
…
After PR 10: cross-device push notifications                          (V1 SHIPS)

Notes for reviewers

  • Drag-tab-out is via right-click menu, not native HTML5 DnD. Native drag-and-drop with iframe state preservation through DOM portals has known React-vs-DOM mutation hazards; explicitly deferred. Right-click on any tab → "Move tab to window…" → pick destination or "+ New window".
  • Address-bar @ and ! prefixes are stubs in PR 4. PR 6 will wire @<agent> to agent suggestions; PR 5 will wire !<profile> to inline profile switching.
  • Find-in-page is wired but inert until PR 6's copilot.js responds to the iframe postMessage. Match count shows nothing.
  • scrollY is on the Tab type but never written — placeholder for the discard-then-reload restore. Will be wired when PR 6's copilot.js can postMessage scroll position back.
  • Cookie key is still the per-install random key from PR 3's last fix-up. PR 5+ will derive per-user from login password.
  • 4 final-review fixes landed in commit e03e9a5: Cmd+L focus event, BrowserApp unmount cleanup (no orphan windows), moveTab index-clamping bug, and per-window keyboard-handler scoping (was firing N× with N windows open).

Summary by CodeRabbit

New Features

  • Complete browser app redesign with improved tab management and window support
  • Address bar with autocomplete suggestions from history and bookmarks
  • Find in page search functionality
  • Keyboard shortcuts for common actions (new tab, close tab, find, zoom)
  • Multiple window management with window switching UI
  • Tab pinning and moving between windows
  • Session persistence for browser windows and tabs
  • Tab memory optimization with automatic discarding of inactive tabs

jaylfc added 14 commits May 4, 2026 00:49
…oping

Whole-branch Opus review caught 1 Critical + 3 Important issues:

- AddressBar (Critical): Cmd+L dispatches taos-browser:focus-address
  but no listener existed. Now AddressBar subscribes to the event
  with windowId match + focuses the input. Cmd+L works again.
- BrowserApp (Important): closed browser windows left orphan entries
  in browser-store and persisted server rows that grew unboundedly.
  Now the BrowserApp unmount cleanup calls removeWindow + the
  backend deleteWindow.
- browser-store moveTab (Important): Math.min(0, fromTabs.length-1)
  always returns 0 — should clamp by the original closing index
  matching closeTab's next-tab-by-index semantics. Test added for
  the active-tab-out-of-multi-tab-source case that was uncovered.
- BrowserApp (Important): keyboard hook always received hasFocus=true,
  causing every shortcut to fire N× with N browser windows open.
  Now reads focused state from process-store so only the focused
  window's shortcuts fire.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@jaylfc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 33 minutes and 49 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ed5f7a7-0fb4-4574-89f0-880c12045dd7

📥 Commits

Reviewing files that changed from the base of the PR and between e03e9a5 and 1b7bf4b.

📒 Files selected for processing (14)
  • desktop/src/apps/BrowserApp/AddressBar.test.tsx
  • desktop/src/apps/BrowserApp/AddressBar.tsx
  • desktop/src/apps/BrowserApp/AddressSuggest.tsx
  • desktop/src/apps/BrowserApp/Chrome.tsx
  • desktop/src/apps/BrowserApp/FindInPage.tsx
  • desktop/src/apps/BrowserApp/MoveTabMenu.tsx
  • desktop/src/apps/BrowserApp/TabOverview.tsx
  • desktop/src/apps/BrowserApp/TabRenderer.tsx
  • desktop/src/apps/BrowserApp/TabStrip.tsx
  • desktop/src/apps/BrowserApp/WindowChooser.tsx
  • desktop/src/apps/BrowserApp/keyboard.ts
  • tests/routes/desktop_browser/test_suggest.py
  • tinyagentos/routes/desktop_browser/suggest.py
  • tinyagentos/routes/desktop_browser/windows.py
📝 Walkthrough

Walkthrough

The PR refactors BrowserApp from a monolithic component to a modular, store-driven architecture. It removes the old BrowserApp.tsx, introduces a new directory-based structure with specialized components (Chrome, TabStrip, AddressBar, TabRenderer, FindInPage, TabOverview, WindowChooser), adds a Zustand browser store for window/tab state management, integrates session persistence for browser windows, and provides backend APIs for address suggestions and window state persistence.

Changes

BrowserApp v2 Frontend Refactor

Layer / File(s) Summary
Data Shape & Types
desktop/src/apps/BrowserApp/types.ts
Introduces Tab, RecentlyClosedTab, and BrowserWindowState interfaces defining window/tab/navigation data structures with states (live/discarded) and exclusion rules.
Store Implementation
desktop/src/stores/browser-store.ts, desktop/src/stores/browser-store.test.ts
Zustand store useBrowserStore manages per-window state (tabs, active tab, recently closed). Provides actions for window/tab CRUD, navigation history, pinning, discard scheduling, zoom, and cross-window tab movement. Includes comprehensive test suite.
Component Layer
desktop/src/apps/BrowserApp/BrowserApp.tsx, desktop/src/apps/BrowserApp/Chrome.tsx, desktop/src/apps/BrowserApp/TabStrip.tsx, desktop/src/apps/BrowserApp/AddressBar.tsx
New BrowserApp top-level component handles window lifecycle and layout; Chrome renders navigation toolbar; TabStrip renders tab bar with pinning support; AddressBar manages focused URL input with debounced suggestion fetching.
Iframe & Tab Rendering
desktop/src/apps/BrowserApp/TabRenderer.tsx, desktop/src/apps/BrowserApp/TabRenderer.test.tsx
TabRenderer pools iframes for live tabs (display:none for inactive), implements discard scheduler for idle non-pinned tabs after 10m, enforces max-live-tabs cap (12), and proxies iframe src via /api/desktop/browser/proxy.
Mobile UI & Overlays
desktop/src/apps/BrowserApp/TabOverview.tsx, desktop/src/apps/BrowserApp/WindowChooser.tsx, desktop/src/apps/BrowserApp/FindInPage.tsx, desktop/src/apps/BrowserApp/MoveTabMenu.tsx
Mobile fallback tab selection, multi-window chooser with creation, in-page find with iframe message broadcasting, and context menu for moving tabs between windows.
Address Suggestions
desktop/src/apps/BrowserApp/AddressSuggest.tsx, desktop/src/apps/BrowserApp/AddressSuggest.test.tsx
Popover listbox component for rendering and selecting address autocomplete suggestions.
Keyboard Shortcuts
desktop/src/apps/BrowserApp/keyboard.ts, desktop/src/apps/BrowserApp/keyboard.test.ts
useBrowserKeyboardShortcuts hook dispatches platform-aware shortcut actions (Cmd/Ctrl+T/W/F, zoom +/−/reset) when window is focused.
Component Tests
desktop/src/apps/BrowserApp/*.test.tsx
Test suites for each component covering rendering, user interactions, store integration, and edge cases.
Exports
desktop/src/apps/BrowserApp/index.ts
Barrel module re-exporting BrowserApp to maintain stable import paths.

Frontend-Backend Integration

Layer / File(s) Summary
Suggestion API Client
desktop/src/lib/browser-suggest-api.ts, desktop/src/lib/browser-suggest-api.test.ts
fetchSuggestions(profileId, q, limit) client fetches address-bar autocomplete suggestions from /api/desktop/browser/suggest.
Window Persistence API Client
desktop/src/lib/browser-windows-api.ts, desktop/src/lib/browser-windows-api.test.ts
loadWindows(), saveWindows(windows), deleteWindow(windowId) clients manage browser-window persistence via /api/desktop/browser/windows GET/PUT/DELETE.
Session Persistence Integration
desktop/src/hooks/use-session-persistence.ts
Extends useSessionPersistence to restore browser windows on mount via loadBrowserWindows() and auto-save to backend after 2s of inactivity via saveBrowserWindows().

Backend Services

Layer / File(s) Summary
Store Methods
tinyagentos/routes/desktop_browser/store.py
BrowserStore adds upsert_window(), list_windows(), delete_window() for window persistence and add_history(), search_history(), add_bookmark(), list_bookmarks() for local suggestion sources.
Suggest Endpoint
tinyagentos/routes/desktop_browser/suggest.py, tests/routes/desktop_browser/test_suggest.py
GET /api/desktop/browser/suggest returns bookmarks (prioritized) and deduped history suggestions for an address-bar query, excluding @-prefixed reserved queries. Per-user isolation enforced.
Windows Endpoints
tinyagentos/routes/desktop_browser/windows.py, tests/routes/desktop_browser/test_windows.py
GET /api/desktop/browser/windows lists user's persisted windows; PUT upserts a bulk snapshot; DELETE /{window_id} removes a window. All endpoints authenticated and per-user isolated.
Route Registration
tinyagentos/routes/desktop_browser/__init__.py
Registers new suggest and windows route modules alongside existing proxy.
Proxy Removal
tinyagentos/routes/desktop.py
Removes deprecated /api/desktop/proxy endpoint and related HTML-rewriting helpers; cleans up unused imports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 From monolith to modules bright,
Zustand stores and tabs in flight,
Windows pooled and suggestions flow,
Persistence deep, from front to toe!
Refactored dreams come alive tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(browser): frontend chrome rebuild — multi-window + tab model (PR 4/10)' clearly and specifically describes the main change: a comprehensive rebuilding of the browser frontend with multi-window and multi-tab functionality.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/browser-pr-4-frontend-chrome

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
Review rate limit: 0/1 reviews remaining, refill in 33 minutes and 49 seconds.

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

function resolveFinalUrl(input: string): string {
if (input.startsWith("@") || input.startsWith("!")) {
// No-op for PR 4 — PR 5/6 will replace this branch
return input;
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: Potential security vulnerability in stub implementation

The @ and ! prefix handling is currently a stub (PR 4), returning the input unvalidated. This could allow javascript: URLs to be navigated to if a user types '@javascript:alert(1)', as it bypasses the search query logic and navigates directly.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 4, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (11 files)
  • desktop/src/apps/BrowserApp/AddressBar.test.tsx
  • desktop/src/apps/BrowserApp/AddressBar.tsx
  • desktop/src/apps/BrowserApp/AddressSuggest.tsx
  • desktop/src/apps/BrowserApp/Chrome.tsx
  • desktop/src/apps/BrowserApp/FindInPage.tsx
  • desktop/src/apps/BrowserApp/MoveTabMenu.tsx
  • desktop/src/apps/BrowserApp/TabOverview.tsx
  • desktop/src/apps/BrowserApp/TabRenderer.tsx
  • desktop/src/apps/BrowserApp/TabStrip.tsx
  • desktop/src/apps/BrowserApp/WindowChooser.tsx
  • desktop/src/apps/BrowserApp/keyboard.ts
  • tests/routes/desktop_browser/test_suggest.py
  • tinyagentos/routes/desktop_browser/suggest.py
  • tinyagentos/routes/desktop_browser/windows.py

Fix these issues in Kilo Cloud


Reviewed by grok-code-fast-1:optimized:free · 154,261 tokens

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (3)
desktop/src/apps/BrowserApp/MoveTabMenu.test.tsx (1)

16-77: ⚡ Quick win

Consider adding a test for the "New window" path once the queueMicrotask race is fixed.

The three existing cases are solid. The "New window" click in handleNewWindow is the only untested branch — and it's precisely where the race condition identified in MoveTabMenu.tsx manifests. A test that asserts moveTab is called with the new window ID after the store entry appears would both document the fix and guard against regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/BrowserApp/MoveTabMenu.test.tsx` around lines 16 - 77, Add a
test that exercises the "New window" path of MoveTabMenu by spying on
useBrowserStore.getState().moveTab, rendering MoveTabMenu with a single source
window, clicking the "New window" menu item (which triggers
MoveTabMenu.handleNewWindow), then waiting for the store to contain the newly
created window ID and asserting moveTab was called with ("fromWindowId", tabId,
newWindowId) and that onClose was invoked; use waitFor/act to allow the
queueMicrotask-created store entry to settle before asserting so the microtask
race is avoided.
tinyagentos/routes/desktop_browser/store.py (1)

172-172: ⚡ Quick win

LIKE metacharacters in query are not escaped — searches with % or _ behave unexpectedly.

f"%{query}%" passes the raw user string into the LIKE pattern. A % symbol in a LIKE pattern matches any sequence of zero or more characters, and _ matches any single character. So a user typing % in the address bar gets like = "%%%" which matches every row (up to limit). The ESCAPE clause is needed to treat these literally.

Fix both search_history (line 172) and list_bookmarks (line 221):

🔧 Proposed fix
+def _escape_like(s: str) -> str:
+    return s.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")


 async def search_history(self, *, user_id, profile_id, query, limit=8):
     ...
-    like = f"%{query}%"
+    like = f"%{_escape_like(query)}%"
     cursor = await self._db.execute(
         "SELECT url, title, visited_at "
         "FROM history "
         "WHERE user_id = ? AND profile_id = ? "
-        "  AND (url LIKE ? OR title LIKE ?) "
+        "  AND (url LIKE ? OR title LIKE ?) ESCAPE '\\' "
         "ORDER BY visited_at DESC LIMIT ?",
         (user_id, profile_id, like, like, limit),
     )

Apply the same change to the if query: branch in list_bookmarks.

Also applies to: 221-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/routes/desktop_browser/store.py` at line 172, search_history
builds the LIKE pattern from raw query which lets user '%' or '_' act as
wildcards; escape any existing backslashes then percent and underscore in query
(e.g., escape \ then replace % -> \% and _ -> \_) before creating like =
f"%{escaped}%", and add an ESCAPE '\\' clause to the SQL so the
backslash-escaped characters are treated literally; apply the same change to the
list_bookmarks branch that builds its like pattern as well, using the same
escaped variable and ESCAPE '\\' in the query.
desktop/src/apps/BrowserApp/TabRenderer.tsx (1)

108-110: ⚖️ Poor tradeoff

Per-tab zoom via scale() clips content rather than reflowing it.

transform: scale(N) on an inset-0 absolute iframe visually magnifies from the top-left corner; the iframe still renders for the full unscaled viewport. At zoom > 1 the bottom-right portion of every page becomes unreachable (clipped by the parent's overflow-hidden) with no way for the user to scroll to it. Users expect browser zoom to reflow the page at a new pixel density, not to crop it.

Consider tracking this as a known PR-4 limitation and deferring a proper implementation (e.g., applying a zoom property inside the iframe's document, or a viewport meta-tag approach) to the next PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/BrowserApp/TabRenderer.tsx` around lines 108 - 110, The
per-tab zoom currently uses CSS transform in TabRenderer (transform: tab.zoom
!== 1 ? `scale(${tab.zoom})`), which clips iframe content; remove/disable this
transform-based scaling so the iframe is not cropped, add a TODO and reference
PR-4 to defer a proper implementation, and instead persist the desired zoom
value (tab.zoom) for later application inside the iframe (e.g., via a
postMessage to the iframe content to set document.body.style.zoom or inject a
viewport meta approach). Update TabRenderer to stop applying transform scaling
when tab.zoom !== 1, add a concise comment noting PR-4 and the intended
in-iframe fix, and ensure any UI/state still exposes tab.zoom for the follow-up
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/apps/BrowserApp/AddressBar.tsx`:
- Around line 84-92: commitNavigation calls navigateTab with raw stub inputs
like "@john" or "!foo" which causes browsers to attempt a relative navigation;
update commitNavigation to detect stub inputs (e.g., strings starting with "@"
or "!") after trimming and early-return (but still clear suggestions and reset
selected index) instead of calling navigateTab; reference the commitNavigation
function and leave resolveFinalUrl unchanged for now and then remove the
now-unreachable stub branch in resolveFinalUrl (the branch handling "@" / "!")
as a follow-up.

In `@desktop/src/apps/BrowserApp/AddressSuggest.tsx`:
- Around line 52-72: The suggestions list currently renders each item as <button
role="option"> which breaks ARIA listbox semantics; change the element returned
in AddressSuggest (the mapped suggestion item) from a button to a
non-interactive container (e.g. a div) with tabIndex={-1} and keep the existing
props: role="option", aria-selected, data-suggest-index, onMouseEnter={() =>
onHighlight(i)} and onClick={() => onSelect(s)}; remove button-specific
attributes like type and rely on the parent AddressBar keyboard handling (keep
Icon, span title/url markup and className logic intact) so the input remains the
single tab stop and listbox option semantics are preserved.

In `@desktop/src/apps/BrowserApp/Chrome.tsx`:
- Around line 84-88: The profile chip div currently uses role="status" which
creates an unnecessary live region; remove the role="status" attribute from the
div (the element with className "flex items-center gap-1.5 px-2 py-0.5
rounded-full bg-shell-bg-deep border border-shell-border-subtle text-xs" and
aria-label={`Profile: ${win.profileId}`}) so it becomes a static, display-only
badge; if you later add a decorative icon consider replacing with role="img" and
a descriptive aria-label, but do not leave role="status".

In `@desktop/src/apps/BrowserApp/FindInPage.tsx`:
- Around line 45-65: The broadcastQuery function currently calls
iframe.contentWindow?.postMessage(..., "*") which leaks search queries to any
cross-origin iframe; change the call to use window.location.origin as the
targetOrigin (i.e., pass the app's origin instead of "*") so messages are only
delivered to same-origin/proxied frames, update the comment to reflect that
cross-origin frames will be silently ignored, and keep the existing try/catch
around iframe.contentWindow?.postMessage to swallow messaging errors for
unreachable frames.

In `@desktop/src/apps/BrowserApp/keyboard.ts`:
- Around line 74-92: The zoom handlers for "+"/"=" and "-"/"_" should clamp and
round values before calling store.setTabZoom: compute newZoom = currentZoom ±
ZOOM_STEP (or 1.0 for "0"), clamp newZoom into a safe range (e.g. minZoom =
0.25, maxZoom = 5.0) and then round to two decimal places to avoid
floating-point accumulation (use a rounding helper or
Number(parseFloat(...).toFixed(2))). Update the calls in the switch cases that
reference activeTab, ZOOM_STEP and store.setTabZoom to use the
clamped-and-rounded newZoom while keeping e.preventDefault() and the same
activeTab/windowId arguments.

In `@desktop/src/apps/BrowserApp/MoveTabMenu.tsx`:
- Around line 53-63: handleNewWindow currently uses queueMicrotask so moveTab
runs before the new BrowserApp has mounted and created its window entry, causing
moveTab to silently no-op; instead, after calling openWindow("browser",
browserApp.defaultSize) subscribe to the browser store (or use the same store
selector used by moveTab) and wait until windows[newWindowId] exists, then call
moveTab(fromWindowId, tabId, newWindowId), unsubscribe the listener, and finally
call onClose; reference handleNewWindow, openWindow, moveTab,
BrowserApp.useEffect/createWindow and the browser store's windows map when
implementing the polling/subscription and cleanup.

In `@desktop/src/apps/BrowserApp/TabOverview.tsx`:
- Line 51: The grid containers wrapping TabCard items are missing
role="tablist", leaving each element using role="tab" and aria-selected
orphaned; update the two grid wrapper divs in TabOverview.tsx (the elements with
className "grid grid-cols-2 gap-2" and the second similar grid at the later
section) to include role="tablist" so the TabCard components (which render
role="tab" / aria-selected) are proper children of a tablist and accessible to
screen readers.

In `@desktop/src/apps/BrowserApp/TabRenderer.tsx`:
- Line 100: The iframe sandbox in TabRenderer.tsx currently includes
"allow-scripts allow-same-origin", which nullifies the sandbox when the proxied
content is same-origin; update the TabRenderer iframe sandbox attribute to
remove "allow-same-origin" (keep only safe tokens like "allow-scripts
allow-forms allow-popups allow-downloads" or whatever minimal set is required)
so framed pages cannot escape to the parent, and if you truly need
same-origin/script execution for proxied content, instead serve the proxy from
an isolated subdomain (e.g., browser-sandbox) so you can safely drop
allow-same-origin; adjust any proxy routing for /api/desktop/browser/proxy
accordingly and document the change in TabRenderer.

In `@desktop/src/apps/BrowserApp/TabStrip.tsx`:
- Around line 116-123: The tab wrapper div in TabStrip.tsx is missing the
Tailwind "group" class so group-hover variants on the close button never
trigger; update the className array used for the TabItem wrapper (the element
composed from widthClass, the "h-[28px] px-2 ..." string, and isActive branch)
to include "group" (e.g., add "group" to that array) so group-hover:opacity-100
on the close button can reveal it on tab hover.

In `@desktop/src/apps/BrowserApp/WindowChooser.tsx`:
- Around line 80-108: The ul currently rendered in WindowChooser.tsx uses
role="list" but contains buttons with role="option"; update the parent element
to use role="listbox" so the option ownership is correct for assistive tech.
Locate the JSX rendering the list (the ul that maps over windowList and renders
items via summarizeWindow) and change its role to "listbox"; keep existing
button elements (role="option", aria-selected) and the onClick handlers
(onSelect, onClose) intact so tests querying getAllByRole("option") and
selection behavior remain unchanged.

In `@tests/routes/desktop_browser/test_suggest.py`:
- Around line 117-122: The test currently uses a non-strict assertion (assert
len(suggestions) <= 5) which won't catch cases where limit isn't enforced;
update the assertion to assert len(suggestions) == 5 to verify the limit=5
parameter is applied, keeping the rest of the request (client.get to
"/api/desktop/browser/suggest" with params
{"profile_id":"personal","q":"example","limit":5}) and the suggestions variable
unchanged; also ensure the test setup indeed inserts 15 distinct matching
entries so the equality assertion is meaningful.

In `@tinyagentos/routes/desktop_browser/suggest.py`:
- Around line 23-24: Suppress the false-positive Ruff B008 on the FastAPI
dependency by either adding fastapi.Depends to the ruff config's
extend-immutable-calls (e.g., add "fastapi.Depends" to
tool.ruff.lint.flake8-bugbear.extend-immutable-calls) or by silencing the
warning inline where Depends(get_current_user) is used; additionally, enforce a
hard cap on the limit parameter (e.g., clamp or validate limit in the route
handler or dependency) so callers cannot pass extremely large values that cause
list_bookmarks or search_history to allocate huge result sets (use the limit
parameter name and the route handler that calls list_bookmarks/search_history to
apply the clamp/validation).

In `@tinyagentos/routes/desktop_browser/windows.py`:
- Line 33: Add a Ruff B008 suppression for FastAPI dependency calls: update the
route handler parameter annotations that use Depends(get_current_user) (and the
similar Depends(...) usages at the other two occurrences) to include "# noqa:
B008" on those lines, or alternatively add "B008" to the project-level Ruff
extend-ignore; target the function/method signatures that reference Depends and
get_current_user to locate the exact places to change.

---

Nitpick comments:
In `@desktop/src/apps/BrowserApp/MoveTabMenu.test.tsx`:
- Around line 16-77: Add a test that exercises the "New window" path of
MoveTabMenu by spying on useBrowserStore.getState().moveTab, rendering
MoveTabMenu with a single source window, clicking the "New window" menu item
(which triggers MoveTabMenu.handleNewWindow), then waiting for the store to
contain the newly created window ID and asserting moveTab was called with
("fromWindowId", tabId, newWindowId) and that onClose was invoked; use
waitFor/act to allow the queueMicrotask-created store entry to settle before
asserting so the microtask race is avoided.

In `@desktop/src/apps/BrowserApp/TabRenderer.tsx`:
- Around line 108-110: The per-tab zoom currently uses CSS transform in
TabRenderer (transform: tab.zoom !== 1 ? `scale(${tab.zoom})`), which clips
iframe content; remove/disable this transform-based scaling so the iframe is not
cropped, add a TODO and reference PR-4 to defer a proper implementation, and
instead persist the desired zoom value (tab.zoom) for later application inside
the iframe (e.g., via a postMessage to the iframe content to set
document.body.style.zoom or inject a viewport meta approach). Update TabRenderer
to stop applying transform scaling when tab.zoom !== 1, add a concise comment
noting PR-4 and the intended in-iframe fix, and ensure any UI/state still
exposes tab.zoom for the follow-up change.

In `@tinyagentos/routes/desktop_browser/store.py`:
- Line 172: search_history builds the LIKE pattern from raw query which lets
user '%' or '_' act as wildcards; escape any existing backslashes then percent
and underscore in query (e.g., escape \ then replace % -> \% and _ -> \_) before
creating like = f"%{escaped}%", and add an ESCAPE '\\' clause to the SQL so the
backslash-escaped characters are treated literally; apply the same change to the
list_bookmarks branch that builds its like pattern as well, using the same
escaped variable and ESCAPE '\\' in the query.
🪄 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: e250490a-7cc8-4275-8123-f8bd320926e3

📥 Commits

Reviewing files that changed from the base of the PR and between 13f42d6 and e03e9a5.

📒 Files selected for processing (40)
  • desktop/src/apps/BrowserApp.initialUrl.test.tsx
  • desktop/src/apps/BrowserApp.tsx
  • desktop/src/apps/BrowserApp/AddressBar.test.tsx
  • desktop/src/apps/BrowserApp/AddressBar.tsx
  • desktop/src/apps/BrowserApp/AddressSuggest.test.tsx
  • desktop/src/apps/BrowserApp/AddressSuggest.tsx
  • desktop/src/apps/BrowserApp/BrowserApp.test.tsx
  • desktop/src/apps/BrowserApp/BrowserApp.tsx
  • desktop/src/apps/BrowserApp/Chrome.test.tsx
  • desktop/src/apps/BrowserApp/Chrome.tsx
  • desktop/src/apps/BrowserApp/FindInPage.test.tsx
  • desktop/src/apps/BrowserApp/FindInPage.tsx
  • desktop/src/apps/BrowserApp/MoveTabMenu.test.tsx
  • desktop/src/apps/BrowserApp/MoveTabMenu.tsx
  • desktop/src/apps/BrowserApp/TabOverview.test.tsx
  • desktop/src/apps/BrowserApp/TabOverview.tsx
  • desktop/src/apps/BrowserApp/TabRenderer.test.tsx
  • desktop/src/apps/BrowserApp/TabRenderer.tsx
  • desktop/src/apps/BrowserApp/TabStrip.test.tsx
  • desktop/src/apps/BrowserApp/TabStrip.tsx
  • desktop/src/apps/BrowserApp/WindowChooser.test.tsx
  • desktop/src/apps/BrowserApp/WindowChooser.tsx
  • desktop/src/apps/BrowserApp/index.ts
  • desktop/src/apps/BrowserApp/keyboard.test.ts
  • desktop/src/apps/BrowserApp/keyboard.ts
  • desktop/src/apps/BrowserApp/types.ts
  • desktop/src/hooks/use-session-persistence.ts
  • desktop/src/lib/browser-suggest-api.test.ts
  • desktop/src/lib/browser-suggest-api.ts
  • desktop/src/lib/browser-windows-api.test.ts
  • desktop/src/lib/browser-windows-api.ts
  • desktop/src/stores/browser-store.test.ts
  • desktop/src/stores/browser-store.ts
  • tests/routes/desktop_browser/test_suggest.py
  • tests/routes/desktop_browser/test_windows.py
  • tinyagentos/routes/desktop.py
  • tinyagentos/routes/desktop_browser/__init__.py
  • tinyagentos/routes/desktop_browser/store.py
  • tinyagentos/routes/desktop_browser/suggest.py
  • tinyagentos/routes/desktop_browser/windows.py
💤 Files with no reviewable changes (2)
  • desktop/src/apps/BrowserApp.initialUrl.test.tsx
  • desktop/src/apps/BrowserApp.tsx

Comment thread desktop/src/apps/BrowserApp/AddressBar.tsx
Comment thread desktop/src/apps/BrowserApp/AddressSuggest.tsx Outdated
Comment thread desktop/src/apps/BrowserApp/Chrome.tsx
Comment thread desktop/src/apps/BrowserApp/FindInPage.tsx
Comment on lines +74 to +92
case "+":
case "=":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, activeTab.zoom + ZOOM_STEP);
}
break;
case "-":
case "_":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, activeTab.zoom - ZOOM_STEP);
}
break;
case "0":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, 1.0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Zoom has no bounds clamping and accumulates floating-point error

Two issues in the same block:

  1. No clamping: zoom - ZOOM_STEP can reach 0 or go negative after enough presses; zoom + ZOOM_STEP is unbounded. A negative zoom would likely produce a broken CSS transform: scale(...). Should clamp to a sensible range (e.g. [0.25, 5.0]).

  2. FP accumulation: 1.0 + 0.1 = 1.1, but 1.1 + 0.1 = 1.2000000000000002. If the zoom value is displayed as a percentage label (e.g. "120%"), this will surface as "120.00000000002%". Rounding to two decimal places before passing to the store fixes this.

🔧 Proposed fix
+const MIN_ZOOM = 0.25;
+const MAX_ZOOM = 5.0;
+
+function clampZoom(z: number): number {
+  return Math.round(Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z)) * 100) / 100;
+}
        case "+":
        case "=":
          if (activeTab) {
            e.preventDefault();
-           store.setTabZoom(windowId, activeTab.id, activeTab.zoom + ZOOM_STEP);
+           store.setTabZoom(windowId, activeTab.id, clampZoom(activeTab.zoom + ZOOM_STEP));
          }
          break;
        case "-":
        case "_":
          if (activeTab) {
            e.preventDefault();
-           store.setTabZoom(windowId, activeTab.id, activeTab.zoom - ZOOM_STEP);
+           store.setTabZoom(windowId, activeTab.id, clampZoom(activeTab.zoom - ZOOM_STEP));
          }
          break;
📝 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
case "+":
case "=":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, activeTab.zoom + ZOOM_STEP);
}
break;
case "-":
case "_":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, activeTab.zoom - ZOOM_STEP);
}
break;
case "0":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, 1.0);
}
const MIN_ZOOM = 0.25;
const MAX_ZOOM = 5.0;
function clampZoom(z: number): number {
return Math.round(Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z)) * 100) / 100;
}
case "+":
case "=":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, clampZoom(activeTab.zoom + ZOOM_STEP));
}
break;
case "-":
case "_":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, clampZoom(activeTab.zoom - ZOOM_STEP));
}
break;
case "0":
if (activeTab) {
e.preventDefault();
store.setTabZoom(windowId, activeTab.id, 1.0);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/BrowserApp/keyboard.ts` around lines 74 - 92, The zoom
handlers for "+"/"=" and "-"/"_" should clamp and round values before calling
store.setTabZoom: compute newZoom = currentZoom ± ZOOM_STEP (or 1.0 for "0"),
clamp newZoom into a safe range (e.g. minZoom = 0.25, maxZoom = 5.0) and then
round to two decimal places to avoid floating-point accumulation (use a rounding
helper or Number(parseFloat(...).toFixed(2))). Update the calls in the switch
cases that reference activeTab, ZOOM_STEP and store.setTabZoom to use the
clamped-and-rounded newZoom while keeping e.preventDefault() and the same
activeTab/windowId arguments.

Comment thread desktop/src/apps/BrowserApp/TabStrip.tsx
Comment thread desktop/src/apps/BrowserApp/WindowChooser.tsx Outdated
Comment thread tests/routes/desktop_browser/test_suggest.py Outdated
Comment thread tinyagentos/routes/desktop_browser/suggest.py Outdated
Comment thread tinyagentos/routes/desktop_browser/windows.py Outdated
…inor)

Major:
- TabRenderer: drop iframe sandbox `allow-same-origin`. Combined with
  `allow-scripts` on a same-origin proxy, it was a sandbox bypass.
  Isolated-subdomain proxy hosting deferred to HTTPS+DNS Foundations
  brainstorm (already documented in PR 2's spec amendment).
- TabStrip: add missing `group` Tailwind class so close-button
  `group-hover:opacity-100` actually fires (button was permanently
  invisible — tests didn't catch it because RTL queries by aria-label
  regardless of visibility).
- MoveTabMenu: replace queueMicrotask race with a real store
  subscription. queueMicrotask fired before the new BrowserApp's
  mount-effect could call createWindow, so moveTab was a silent no-op.

Minor (security + a11y + lint):
- AddressBar: @/! prefixes are now true no-ops (don't call navigateTab
  with raw input). Defends against `@javascript:...` constructions.
- AddressSuggest: <button role="option"> → <div role="option"
  tabIndex={-1}>. Buttons inside listboxes confuse keyboard semantics.
- Chrome: remove role="status" from profile chip — live region would
  spuriously announce when PR 5 wires switching.
- FindInPage: postMessage targetOrigin "*" → window.location.origin.
  Stops search-query leakage to genuinely cross-origin embedded pages.
- keyboard.ts zoom: round to 1 decimal to stop 0.30000000000000004
  drift from repeated +0.1 / -0.1.
- TabOverview: add role="tablist" to the grid containers so
  role="tab" children have a proper owner.
- WindowChooser: <ul role="list"> → role="listbox" so role="option"
  buttons have a valid owner.
- test_suggest: <= 5 → == 5 for limit-cap assertion.
- suggest.py + windows.py: # noqa: B008 on Depends() defaults.
- suggest.py: cap limit to [1, 50] to prevent abuse.
@jaylfc jaylfc merged commit 1dd64af into master May 4, 2026
8 checks passed
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.

1 participant