Skip to content

v0.2.8

Pre-release
Pre-release

Choose a tag to compare

@AbdelrahmanBerchan AbdelrahmanBerchan released this 26 May 15:00
· 41 commits to main since this release
2b1916a

Axis Browser v0.2.8

Privacy / browsing

  • Dependency ip-address (CVE-2024-42338): npm overrides pins ip-address to ^10.2.0 (transitive via socks / dev tooling) so XSS in Address6 HTML-emitting methods is patched; lockfile resolves 10.2.0.
  • Dependency fast-uri (CVE-2024-6321 + host-confusion advisory): npm overrides pins fast-uri to ^3.1.2 (transitive via electron-builder / electron-store / ajv) so path traversal via percent-encoded dot segments (GHSA-q3j6-qgpj-74h6) and host confusion via percent-encoded authority (GHSA-v39h-62p7-jpjc) are patched; lockfile resolves 3.1.2.
  • Ad blocker: Network and cosmetic blocking via @ghostery/adblocker-electron (cached lists under user data). Toggle with the shield in the URL bar (left of copy link) or Settings → General → Ad blocker; default is on.
  • Guest context menu — Save Image: Right-click on an image includes Save Image; it runs webContents.downloadURL on the guest <webview> (save-image-from-url) so the normal download flow and session cookies apply (with shell fallback if the guest id is unavailable). The in-app #webpage-save-image row matches for any HTML fallback menu.
  • Guest context menu — image open / copy: Open Image in New Tab, Copy Image, and Copy Image Address work for relative and safe data:image/* URLs (resolved against file:, axis:, http(s), etc., whenever the page URL parses); SVG and AVIF data: URIs are included. New tabs use a trusted image navigate path so data: is not dropped by sanitizeUrl. Copy Image uses main copy-image-at-guest (webContents.copyImageAt) with the menu’s guest id and coordinates, with <webview>.copyImageAt as fallback; the native menu IPC forwards pageURL and guestWebContentsId where needed. Copy Image Address uses writeTextToClipboard (navigator.clipboardexecCommand('copy')write-clipboard-text in main) so copying works after a native context menu (no transient user gesture).
  • Extensions: Install from the Chrome Web Store or Mozilla Add-ons (addons.mozilla.org) via the blue Install in Axis bar on listings, the URL bar install button, or Settings → Extensions (paste Chrome URL/ID, Mozilla URL, or Firefox slug e.g. ublock-origin). Store-triggered .crx / .xpi guest downloads are cancelled; Axis fetches via Google update or Mozilla’s API instead. Firefox .xpi packages unpack like Chrome zips — Firefox-only APIs may still prevent some add-ons from running in Chromium. After install succeeds, the banner hides for that listing. Fallbacks: .crx, or an unpacked folder with manifest.json. Settings → Extensions lists, enables/disables, removes, and opens options when provided. The URL bar puzzle button opens the extensions menu (frosted downloads-style popup). Toolbar extension popups load directly as top-level chrome-extension://… pages in the extension session (no data-URL <webview> wrapper) with contextIsolation: false and a preload shim for missing toolbar/window APIs. Manifest V3 installs now load through a generated runtime compatibility copy that maps actionbrowser_action, host_permissionspermissions, and background.service_worker → an MV2-style background page/scripts with an API shim, so popup UIs like Dark Reader and Chessvision have a running background context in Electron. The compatibility manifest strips MV3-only / unsupported fields (scripting, fontSettings, contextMenus, content-script world) including empty optional_permissions, and uses axis_extension_api_shim.js (not a reserved _ filename). Extension loading uses session.extensions.loadExtension when available; expected MV2 deprecation warnings for generated extension-runtime copies are suppressed, and ad blocker setup ignores Ghostery’s duplicate cosmetic-filter IPC registration after the first successful enable.
  • Downloads popup: While a file is still downloading, the per-row control next to the entry is a cancel (×) that calls DownloadItem.cancel() via cancel-active-download instead of the greyed-out show in folder icon; completed rows still use reveal in folder. The footer Open Downloads button is unchanged. After cancel, the row shows Download cancelled until you close the popup (avoids a fake just now time from the partial file on disk).

Theme / OS appearance

  • macOS menu — View: Enter Full Screen at the bottom uses ⌃⌘F with a custom setFullScreen action (no togglefullscreen role) so it does not duplicate the system-injected full-screen row. Windows/Linux keep F11.
  • macOS window hairline: Removed custom --axis-window-chrome-radius / #app corner clip (it didn’t match NSWindow and read as a rim). Darwin drops the body translateZ/perspective promotion (subpixel vs mask) and hides the grain ::after (mix-blend-mode darkened the bezel). BrowserWindow shape is Electron default (roundedCorners no longer forced in AXIS_MACOS_BROWSER_WINDOW_SHAPE — traffic lights only).
  • Glass theme tint: getThemeAwareGlassAlpha caps pale-theme bumps by windowChromeLight so airy values stay translucent, but adds chroma insurance (lift × HSL saturation + a higher baseline floor) so light blue/pink/etc. still read as the chosen color. approximateGlassSurfaceHex default ambients for bright themes are mixHexColors(theme, cool light) so small-tint contrast math isn’t a flat grey wash. Heavier slider positions still thicken the veil; --shell-ink-rgb wiring unchanged.
  • OS light/dark must not restyle Axis: Electron’s nativeTheme.themeSource defaulted to system, so switching the computer to Light changed prefers-color-scheme, vibrancy, and native form chrome and made the shell look wrong. The app now pins nativeTheme.themeSource to dark so appearance stays stable; Settings → Theme → Appearance (light overlays / settings.html) is unchanged.
  • Settings as a browser tab: Settings (axis://settings) now opens as a normal sidebar tab (gear icon, closeable, ⌘W / undo) instead of a separate BrowserWindow. The tab loads embedded settings.html with a dedicated guest preload (contextIsolation: true, contextBridge, same IPC surface as the old window); menu bar / ⌘, / extension shortcuts focus or create the single Settings tab and can jump to a section (History, Shortcuts, Extensions, etc.). History, shortcuts, site permissions, and extensions refresh when their pane is opened. The in-window title strip is hidden in embedded mode.
  • Settings tab data sections: History, Shortcuts, Site permissions, and Extensions now load store-backed lists in the tab webview — the settings guest always runs JavaScript (even when Disable JavaScript is on for pages), uses the settings preload when the webview is recreated, and retries IPC bootstrap after load without host-side control polling.
  • Settings tab URL bar: On axis://settings, the top bar now follows Settings → Theme → Appearance (light / dark), matching the new tab page shell instead of the user theme color.
  • New tab URL bar (light mode): Fixed the top bar staying dark in light appearance — #webview-url-bar no longer reuses the overlay class new-tab-page (that rule forced background: transparent and showed the dark shell through). Uses dedicated url-bar-internal-shell styling with an explicit white background instead.
  • Settings tab blank page: Fixed switchToTab treating axis://settings history as a loaded guest and skipping settings.html load, which left an empty white webview on open or tab switch.
  • Settings tab interactions: Shortcuts and Extensions controls no longer get trapped in a render/section-refresh loop when the Settings search box is empty, so shortcut recording/toggles and extension buttons respond normally.
  • Settings page bootstrap (settings.html): Theme was effectively “light until async settings load” and only applied when uiTheme was already defined in the store, so it tracked the default stylesheet (and felt like the OS). A sync getSettingsWindowBootstrap IPC now reads uiTheme from electron-store before first paint; missing uiTheme is treated as dark like the main window. Base html / body / color-scheme meta defaults match dark so there is no light flash.
  • Standalone Settings visual refresh: settings.html now uses a strict monochrome shell that follows the in-app light/dark setting: white surfaces in light mode, black surfaces in dark mode, neutral selection states, roomier cards/rows, improved search, buttons, inputs, extension rows, and responsive stacked navigation while preserving existing behavior.
  • Selection “Ask” (highlight) + floating Ask sheet: .ai-selection-button, #ai-popup (input, send, response) now follow :root[data-ui-theme] like the rest of Settings → Theme → Appearance, instead of body.light-theme (which was never set).

Settings — Extensions

  • Pane layout: Browse stores (buttons open Chrome Web Store and Firefox add-ons in Axis), Install from URL or ID, Install from disk, and Installed list.
  • Menu bar (Extensions): The system menu adds Extensions (left of Window): installed extensions show manifest icons where available; rows stay enabled whenever the extension is not turned off in Settings (only disabled extensions are greyed). Choosing one opens its popup or options when present; otherwise an explanatory dialog appears. Add extension (Chrome Web Store) / Add extension (Firefox Add-ons) open store browse pages in the focused browser shell when it is active (otherwise the main window), followed by Manage Extensions… with no extra separator between those actions. The list refreshes when extensions are installed, removed, or toggled.

Settings (copy)

  • Window transparency: windowChromeLight blends opaque → airy endpoints (AXIS_SHELL_CHROME_OPAQUE / AXIS_SHELL_CHROME_TRANSPARENT). The airy endpoint alphas/blur/sat were lowered again so 100 admits more desktop light; above 0 the blend uses 1 − (1−t)^1.85 so mid–high slider values lean further toward airy without widening the widget range.
  • Transparent sites (glass mode): BETA badge moved here from Window transparency — glass / page-background clearing is the experimental surface; window chrome transparency is treated as stable copy.

Security

  • CodeQL settings section navigation: Settings tab section focus no longer builds guest executeJavaScript from interpolated section ids — sections are allowlisted and sent over switch-settings-tab IPC (webview.send) instead.
  • CodeQL URL checks: Context-menu image navigation uses full scheme validation (data: only when _isSafeContextMenuDataImageUrl); sidebar media dock YouTube styling uses isYouTubeHost (parsed hostname) instead of youtube.com substring checks.

New tab page

  • Recover closed tab (⌘Z): sanitizeUrl now treats axis://newtab, axis://settings, and axis:note://… as first-class internal URLs instead of sending them through getSearchUrl, so undo / recover reopen the real new tab (not a Google search for axis://newtab).
  • Enter on “Search or enter a URL”: Bare links (e.g. example.com, www.site.org/path) still resolve with sanitizeUrl; any text with spaces that does not start with http:// or https:// is treated as a search so normal queries are not turned into odd %20 paths.
  • Gray translucent background: After removing the idle webview well, a transparent NTP let the shell read gray instead of black. #new-tab-page is #000000 when transparent-sites-mode is off (default dark glass-off). Settings → Theme → Appearance → Light still uses a transparent NTP via data-ui-theme="light".
  • Transparent sites ignored on NTP: body.transparent-sites-mode .new-tab-page had been set to #000 for the gray fix, so glass mode no longer showed through. That override is transparent again; frosted search / toggle / ask cards were already wired to --axis-nt-* vars.

Sidebar

  • Favorites: Favorites row above pinned tabs (Add to Favorites in the tab menu). Equal-width cells (auto-fit + minmax + 1fr) so each row spans the sidebar; fixed row height, flat fills; active tile and the sidebar profile switcher use --shell-ink-rgb (same as tabs / New Tab) so they stay readable on any shell theme and are no longer forced to light-mode black overlays when uiTheme is light; highlight tracks current tab (re-synced on NTP / internal URLs). Standalone runtime tab per favorite; drag to reorder or drag out to remove. Right-click opens a native context menu (show-favorite-context-menu) — Open, Open in New Tab, Copy Link, Rename…, Change Icon / Reset Icon, Remove from Favorites — instead of instantly deleting the tile.
  • Clear unpinned skips favorites: Clear (separator + floating) no longer closes favorite runtime tabs (isFavoriteTab), so using Clear while a favorite is focused does not tear down that session or corrupt favorite state. After Clear, _forceGuestLayoutSync runs so the active guest picks up the real #webviews-container width (fixes responsive pages stuck in a narrow column beside an empty panel when switching to a favorite with no sidebar row). The same layout flush now runs after closing the last normal tab when focus lands on a favorite runtime tab (_syncAfterTabClose), and favorite switches defer the flush until after updateEmptyState settles.
  • Favorites layout after tab close: Each favorite now owns a hidden sidebar tab host (same focus/layout path as pinned tabs) plus deferred guest bounds sync when reactivated, so closing the last normal tab no longer leaves the favorite page in a narrow column. Returning to a favorite does not reload the guest (getURL() while inactive no longer reassigns src; layout rebind runs only when the viewport is still stale after paint).
  • Clear unpinned icons: Separator and floating Clear controls now use fa-arrow-down at 9px (was fa-chevron-down at 7px, which read as a cropped chevron).
  • Profile switcher (sidebar footer): Single icon trigger at the bottom left of the sidebar (sidebar-footer-profile-row), below the mini player (shows current profile only: fa-user / fa-user-secret). Clicking opens a compact menu to pick Personal or Incognito; the menu opens upward. Behavior is unchanged: clicking the current profile does nothing; choosing Incognito from personal first focuses an existing incognito window, and only creates one if none exists. Removed the large in-sidebar Incognito banner.
  • Profile switcher interaction: Sidebar profile trigger/menu now explicitly use -webkit-app-region: no-drag (and pointer-enabled layering), so clicks are no longer swallowed by the sidebar drag region and the menu is fully interactive.
  • Profile switcher (incognito → personal): Personal selection now focuses an existing personal window or creates one if none exists, without forcing open-url-in-browser navigation (so it no longer spawns an unexpected google.com tab).
  • Clear unpinned (no pinned state): Floating Clear (when the separator is hidden) sits in sidebar-tabs-topbar on the traffic-light side (top: 11px); still fades in on sidebar hover and calls clearUnpinnedTabs.
  • Clear unpinned visual parity: Floating no-pinned Clear now uses the same shell-ink opacity states as separator Clear (0.38 default, 0.88 hover, 0.96 active) so both controls match exactly.
  • Clear unpinned in incognito: Floating no-separator Clear visibility now updates from updateEmptyState as well, so it appears correctly in incognito windows where pinned-save flows are bypassed.
  • Clear unpinned in incognito (contrast): Added incognito-specific floating Clear color overrides (rgba(255,255,255, 0.38/0.88/0.96)) so it remains visible on the pure-black incognito sidebar.
  • Extension store install bar recovery: Added a renderer fallback preload path (webview-preload-bundle.js resolved from index.html) when IPC preload-path lookup fails, so Chrome Web Store / Mozilla Add-ons “Install in Axis” banner injection still runs.
  • Extension store install bar fallback (host): Added a host-rendered purple Install in Axis bar in .webview-container that appears on Chrome Web Store / Mozilla Add-ons listing URLs and uses the same install flow as the URL-bar install button. This keeps the banner available even if guest-preload injection is blocked.
  • Extension store host bar reset: Switching to axis://newtab now immediately hides the host install bar (updateExtensionStoreHostBar('')) instead of waiting for the next website navigation.
  • Downloads popup (in-progress states): Items that are still downloading are now visually dimmed/locked, show Downloading… metadata with percentage when total size is known, and render an inline progress bar (indeterminate animation if total is unknown). Row open/reveal actions stay disabled until completion, then return to normal styling/behavior.
  • Downloads popup live tracking source: In-progress state now uses live will-download items (get-active-downloads) instead of persisted download history, so files no longer appear instantly “done”; metadata can include percentage and ETA (min/sec left) when speed/total are available.
  • Downloads popup live updates while open: Progress rows now refresh in place without closing/reopening the popup (updates are driven by axis-download-activity events + a light polling fallback while the popup is visible).
  • Downloads popup no-twitch + dedupe: Live updates now patch existing rows in place (no full list teardown/rebuild), eliminating visible twitch; active-vs-folder entries are deduped by normalized filename (.crdownload/.part/.download stripped) so the same file no longer appears as both greyed and normal.
  • Pinned / unpinned separator: The #tabs-separator line uses a smaller horizontal inset (--tabs-separator-inset-inline) so it runs closer to the left and right edges of the tab column.
  • Mini player after PiP: The dock now appears whenever native PiP closes and the page still has that <video> (not only when playback stayed unpaused). The dock no longer vanishes just because the guest is paused — it hides on ended, missing media, tab close, or Pause and hide.
  • Mini player + resize seam: #sidebar-resize-handle extends to the bottom of the sidebar (was calc(100% - 40px), which stopped above the footer and looked like a broken edge). Footer inset matches the tab column (14px), z-index stacks the dock above the seam.
  • Mini player layout (reference): Expand strip (max-height: 36px) shows only volume / PiP / dismiss at 26×26 (same as the main transport row); expand inner padding matches the main row (6px) so the two control rows line up. No button background highlights; expand inner is transparent + hairline divider. Title: long names use a mask fade into controls (not ); tooltip = full title + Go to tab.
  • Mini player hover / title: Lift, shadow, and expand strip follow pointer hover or keyboard :focus-visible only (not plain :focus-within), so a mouse click no longer leaves the dock “stuck” expanded. Long titles slide toward the end on hover with a light, wide edge feather; YouTube tint is a low-contrast left wash (not a strong red band). prefers-reduced-motion: title slide disabled; expand stays open for reachability. Title (“Go to tab”) uses preventDefault / stopPropagation so it reliably switches tabs instead of behaving like dismiss.
  • Mini player expand controls: Mute and PiP sit on the left; Pause and hide (×) stays on the right. PiP from the dock skips auto-PiP layout/visibility filters (they failed for background tabs) and calls executeJavaScript(..., true) so requestPictureInPicture runs with user gesture; exits an existing PiP element when it is not the target video.

Tabs / webviews

  • Tab & tab group context menus: Native sidebar menus include Reset Icon after Change Icon — enabled when the tab or tab group has a custom icon (hasCustomIcon); clears stored icon data and restores default favicon / group glyph (tab-context-menu-action / tab-group-context-menu-action reset-icon). Tab reset persists pinned state and syncs favorite tiles for favorite runtime tabs. Tab group Change Icon now updates the row: getOrCreateGroupElement reused DOM without refreshing the header — syncTabGroupElementHeader applies color / icon / name; findTabGroupKey normalizes ids for the emoji picker and applySelectedIcon.
  • URL bar tint accuracy: extractUrlBarTheme scores multiple candidates (top-of-page vote, header/nav, theme-color meta, body surface) with agreement boosts and meta penalty when it disagrees with visible chrome; filters cookie strips, buttons/links, and saturated accent chips; walks effective background up the tree; re-runs once after ~480ms on the active tab for late SPA chrome. Still gated on active webview + sequence so tab switches don’t apply stale tints.
  • Inspect Element: Native (and fallback #webpage-inspect) Inspect Element now calls webview.inspectElement(x, y) with the right-click coordinates from the guest context-menu payload instead of only openDevTools(), so the Elements panel targets the node under the cursor (e.g. a button).
  • Trackpad back / forward (guest): Horizontal wheel accumulation threshold (THRESH_AXIAL ~175) and stricter horizontal vs vertical ratio (HORIZONTAL_DOMINANCE) plus a higher minimum |dx| per event in webview-preload-nav.js / webview-preload-bundle.js so side-to-side page scrolling is less likely to trigger back/forward than before.
  • Pinned tabs persistence: Saving pinned tabs no longer depends on being physically above the separator during DOM transitions; persistence now uses normalized tab IDs and each tab’s actual pinned state, preventing intermittent lost pinned tabs after drag/reorder/close flows.
  • Picture-in-picture (auto): Tab-switch PiP only considers large, visible <video> elements (minimum layout 112×112 px / 12k px² area, intrinsic frame ≥96px short edge when known, checkVisibility / opacity) so tiny animated/decorative clips no longer trigger PiP.
  • Picture-in-picture: Native PiP’s only ends PiP and shows the sidebar mini player when the <video> is still there — the shell cannot tell that from “Back to tab,” so the poll never switches tabs on teardown. The URL bar no longer shows a duplicate Back to tab (#url-bar-pip-back-tab removed); use Go to tab on the sidebar dock or native PiP. Dock title still Go to tab without pausing; Pause and hide pauses and clears the dock. Polling 150 ms. hidePIP clears PiP state.
  • Cmd+F popup controls: Find buttons (.search-btn) no longer inherit site-tinted --text-color / --text-color-secondary; they now use fixed dark-mode values with the existing light-mode overrides, so controls follow UI light/dark mode only.
  • YouTube / SPA reload on tab focus: Switching back to a tab no longer reassigns webview.src when the guest already has a http(s) page loaded — only loads from tab.url when the webview is blank. tab.url syncs from getURL() when it lagged (in-page query/hash changes), so sites like YouTube no longer full-reload and lose playback when refocusing the tab.
  • URL bar transitions: Themed .webview-url-bar again cross-fades tint when a page’s colors are applied (e.g. while loading, before/after extractUrlBarTheme). Tab switches set url-bar--instant-theme for that hand-off only so the bar does not fade between tabs; the class is cleared after sync styling or the first extract for the new tab.
  • Trackpad back / forward: Edge hint only: a simple black circular edge pill + white arrow on the gesture side (Safari-ish); the page does not translate. Horizontal gesture direction: swipe right / positive dx → forward, left / negative dx → back (guest preload + macOS BrowserWindow swipe). Busy gestures queue one follow-up swipe. After the timing window axisNavigateAfterGesture prefers webview.goBack() / goForward() when Chromium reports a stack. Guest wheel thresholds slightly relaxed (webview-preload-bundle.js inlined). app-command browser-backward / browser-forward unchanged (semantic keys).
  • Window resize performance: Resize no longer triggers guest layout pings or executeJavaScript resize on every ResizeObserver / resize frame — only once after resizing settles (~120ms idle); Chromium already resizes autosize guests during the drag. Dropped will-change on tab <webview> shells and removed axis-shell-resizing shell blur toggling that churned styles each frame.
  • Guest pages track window size: #content-area / #single-view / .webview-container now use a flex min-height: 0 chain so the page area doesn’t get stuck after sidebar or window resize; ResizeObserver + window resize nudge Electron’s <webview> layout and fire resize in the active guest so responsive layouts reflow.
  • Transparent sites cleanup: Disabling Transparent sites now restores inline page backgrounds changed by the glass-mode DOM patch, including older already-patched pages. Normal browsing also gets an opaque page canvas behind active webviews, so sites like workspace.google.com no longer show Axis transparency when the setting is off.
  • New tab overlay blocked webviews: Switching away from axis://newtab did not hide #new-tab-page (it stacks at z-index: 50), so the overlay kept covering site webviews — URL bar looked correct while the page stayed blank or showed the wrong layer. switchToTab now always calls updateNewTabPageVisibility(false) unless the active tab is the new-tab URL; navigateToUrlInCurrentTab also hides the overlay when leaving axis://newtab.
  • Downloads button while downloading: The URL bar control swaps the arrow icon for an SVG ringdeterminate fill from aggregate receivedBytes / totalBytes across active items (throttled IPC), or a smooth indeterminate arc when totals are unknown. Tooltip shows Downloading…; aria-busy is set for accessibility.
  • Web panel shadow: #content-area::after still uses --axis-webview-elevation-shadow (hairline + inset rim + symmetric 0 0 blurs — no downward-only fake light). Stack retuned so near-edge opacity carries most of the weight and falloff tops out ~44px instead of a 148px haze — reads thicker, tighter, and more like a resting card than a diffuse glow.
  • Close tab left the page on screen: _applyFocusAfterTabClose nulled currentTab before switchToTab; if the target id didn’t match the Map (string vs number) or focus failed, switchToTab returned without selecting a tab while guests were already removed — sidebar updated, stale webview stayed visible. Focus hand-off now normalizes ids with _normalizeTabMapKey, tries neighbors + unpinned + pinned + DOM order, purges #webviews-container guests with no tab row (_purgeStaleWebviewsInContainer), and switchToTab recovers when the id is stale but other tabs exist. Closing a background tab re-runs _prepareWebviewsForTabSwitch for the active tab.
  • Idle webview area painted a white frosted pane: #webviews-container unconditionally painted --axis-webview-idle-well (linear-gradient + backdrop-filter: blur(44px) saturate(185%) + inset shadows), and webview::before rendered an rgba(255, 255, 255, 0.25) glass overlay; both showed as a white pane wherever the guest stack wasn’t covering pixels. Container backgrounds (base, body.chrome-no-tabs, body.transparent-sites-mode, body.incognito-window) are now transparent with no backdrop / shadow, webview::before is removed entirely, and the no-tabs placeholder webviews drop from opacity: 0.3 to opacity: 0; visibility: hidden so nothing paints where the webview is when no page is active.
  • Squared webview / shadow corners when idle: 2px right-edge radii read almost square on Retina, .empty-state and #webviews-container only rounded the bottom-right while the URL bar was hidden (full-height panel), and a duplicate .webview-container rule forced border-radius: 6px on all corners (fighting the sharp-left seam). Introduced --axis-content-panel-radius (6px) for #content-area::after (lift shadow), .webview-container, .webview-url-bar, #webview / #webviews-container / .empty-state, tab <webview> guests, #webview when the bar is hidden, and the AI chat panel; removed the conflicting 6px all-corners override from the loading block.
  • Content panel had squared left corners: The stack intentionally used 0 radius on the sidebar seam (only the window side was rounded). #content-area::after, .webview-container, .empty-state, .ai-chat-panel, and full-height #webviews-container / #webview / guests now use border-radius: var(--axis-content-panel-radius) on all corners; .webview-url-bar rounds top-left and top-right; the guest stack below the bar keeps bottom-left + bottom-right only so the bar’s bottom edge stays straight.

Full Changelog: v0.2.7...v0.2.8

Axis0.2.8FINALFINAL.mp4