Navigation model rebuilt around two full-width drawers and a minimal header (sessions / title-switcher / usage ring / workspace): - Left sessions drawer: cross-project tree with live status indicators, swipe actions on sessions (rename/archive/delete) and on group headers (project edit / two-step close, worktree delete), reorder-only edit mode with collapsible project cards and draggable worktrees, app-level footer (connected instance, settings, pending web update). - Right workspace drawer: Changes / Files / Terminal / Notes / MCP as pill tabs (inactive tabs icon-only); panes stay mounted once visited. The full desktop file editor serves the Files tab; read/skill tool taps in chat open the file there at the requested line. - Header session switcher on title tap: 10 cross-project recents with live busy/attention indicators and project · branch metadata; the usage ring opens a metadata overlay with an explicit loading state. - The overflow menu is gone on phones (its destinations moved into the drawers); iPad keeps it until its dedicated layout pass. Correctness and continuity: - /auth/session answers bearer-first, so a stale WebView cookie can no longer mask a revoked device token; cold launches classify failures fast and land on an explicit connect screen. - Authoritative session snapshots raise frozen ordering baselines and stale live ranks — recents stay truthful after the app slept. - Cold launches reopen the last active session per instance (persisted pointer, confirmed against a sessions snapshot; a user-opened draft clears it), with a logo hold instead of a draft flash. Also: collapsed pill composer gains the stop control; chat tool rows share one 36px rhythm; Task subtool rows truncate; larger bottom safe area so the composer clears big-screen corner radii; Capacitor build hides About/Update (store updates apply there); widgets link to the sessions drawer with a list icon; MobileApp split into focused modules; five mobile-surface detectors unified; translucent borders normalized to 70%; all new strings translated across the 10 locales. iPad and foldable layouts are intentionally untouched - separate next version PR.
Icon System
OpenChamber uses an SVG sprite-based icon system for performance. Remixicon glyphs and explicitly registered custom glyphs are rendered via a single hidden SVG sprite injected into the DOM, referenced by <use href="#oc-icon-name"/>.
Usage
import { Icon } from "@/components/icon/Icon";
<Icon name="arrow-down-s" className="h-4 w-4" />
<Icon name="loader-4" className="size-4 animate-spin" />
Icon Names
Icons use kebab-case names based on Remixicon. To find an icon name:
- Start from the Remixicon component name:
RiArrowDownSLine - Remove
Riprefix →ArrowDownSLine - Convert to kebab-case →
arrow-down-s
Common suffixes:
Line/Fillare dropped from the sprite name- Numbers are preserved:
RiChat4Line→chat-4
Adding a New Icon
- Import and use it in your code:
<Icon name="new-icon-name" /> - Run
bun run icons:spriteto regenerate the sprite with the new icon - The script scans
packages/ui/srcfor allRiXusages and extracts SVG paths
If the icon doesn't exist in the sprite, the script will warn you.
Custom product glyphs are registered in scripts/generate-icon-sprite.mjs. They must use the shared 24x24 viewbox and currentColor so they match Remixicon sizing and theme behavior.
Sizing
The Icon component does not have a size prop. Use Tailwind classes instead:
<Icon name="arrow-down-s" className="h-4 w-4" /> {/* 16px */}
<Icon name="arrow-down-s" className="size-5" /> {/* 20px */}
<Icon name="arrow-down-s" className="h-6 w-6" /> {/* 24px */}
Type Safety
import type { IconName } from "@/components/icon/icons";
const icon: IconName = "arrow-down-s"; // type-checked
Architecture
sprite.ts— Auto-generated SVG path data (runbun run generate-icon-spriteto regenerate)Icon.tsx— The<Icon>component, injects sprite on first mounticons.ts— TypeScript typeIconName
The sprite is injected as a hidden <svg id="openchamber-icon-sprite"> element containing <symbol> elements. Each <Icon> renders <svg><use href="#oc-{name}"/></svg>.
Migration from @remixicon/react
Old:
import { RiArrowDownSLine } from "@remixicon/react";
<RiArrowDownSLine className="h-4 w-4" />
New:
import { Icon } from "@/components/icon/Icon";
<Icon name="arrow-down-s" className="h-4 w-4" />
If an icon is used as a component reference (not JSX):
// Old: const icon = RiStackLine; return <icon />;
// New: const icon: IconName = "stack"; return <Icon name={icon} />;