Unify window chrome: single host-owned header, browser-style route contract for all islands #35
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_os#35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Window chrome in hero_os is fragmented:
IslandHeaderinternally (Files, Editor, Calendar, Contacts, ~16 native islands)..win-headerbar provided by the window (src/components/window.rs:393).crates/hero_os_app/src/main.rs:1973(contacts|filesystem) opts specific islands into a "headerless + floating hover controls" style (window.rs:491-551).hero_archipelagos/archipelagos/embed/) have no header at all and rely on their embedded UI to self-identify.Result: at least three visually distinct window styles coexist, chrome decisions are made per-island, and the hardcoded opt-in list doesn't scale.
Proposal: the browser model
Collapse chrome and content into the browser's mental model:
document.title/history.pushState<iframe src>+ internal navpostMessage→ window headerThe window always owns the chrome. The app only renders content. One header component, one stylesheet, one behavior — styled as the floating rounded-card header that Files currently has, with close/min/max revealed on hover.
Contract
Native islands (Dioxus)
Receive a
WindowRoutecontext and write to it:Iframe islands
Same contract via
postMessage:The window's header component listens to either source and renders: breadcrumbs + close/min/max + optional action slot.
What this deletes
crates/hero_os_app/src/main.rs:1973src/components/window.rs:393and:491-551→ merged into one pathIslandHeadercall sites inside ~16 native islands (component moves to host-owned).win-header/.win-controlsCSSWhat this unlocks
hero_ui_routesspec.island-header, seewindow.rs:323-345)WindowState)Migration order
WindowRoutecontext +postMessagelistener + unified header component inwindow.rs. Always rendered.IslandHeader, write toWindowRoute. Proof-of-concept for native.postMessageemitter in/hero_agent/ui. Proof-of-concept for iframe.IslandHeadercallers..win-header,.win-controls, the headerless list, and internalIslandHeadercall sites.Each step is independently shippable and each one deletes more code than it adds.
Open questions (to resolve before spec)
Elementinto the header (native), and HTML/button-descriptor JSON via postMessage (iframe)? Or restrict to a fixed schema (icon + label + id) for parity?WindowStateshape.hero_archipelagos/core/src/island_header.rs(so embed islands can still use it standalone outside the OS), or move tohero_os_app?hero_ui_routesspec needed.window.rs:78) currently has a simplified header. Does it use the sameWindowRoutecontext with a different renderer, or keep diverged?Will post questions as comments; once resolved, spec PR follows.
1: no for now. 2: in scope for v1, but needs to feel like native app and not browser back and forward. 3: can remain for now, archipelagos is like our component lib. 4: trust, in prod we wont have iframes eventually. 5: elaborate. 6: unify, should not be diverged. 7: yes. again, to clarify, header doesnt shor routes as urls but breadcrumbs like native os apps. It is important to keep native os feel throughout.
Spec: Unified Window Chrome +
WindowRouteContract1. Goals
2. Data model
Lives in
hero_os_app(window-owned):Fixed schema — no arbitrary Dioxus nodes, no arbitrary HTML. Parity native/iframe.
Extend
WindowState:3. Native API (Dioxus)
Hook in
hero_os_app::window_route:Context provided by the window component to its child (
{island_view}inwindow.rs). Island callsuse_window_route()inside its component and writes whenever its internal state changes.Migration for existing
IslandHeaderusers: delete theIslandHeader { ... }call, replace withuse_window_route().set(...).IslandHeaderthe component stays inhero_archipelagos/corefor standalone embed use (outside hero_os), but no hero_os island calls it.4. Iframe API (postMessage)
Protocol (trust-based; no origin allowlist for now):
The window component attaches a
messagelistener scoped to its iframe (event.source === iframeRef). Tiny JS helperhero-window-route.jsshipped to iframe islands, wrapspostMessagein an ergonomic API:5. URL serialization (option B)
Fragment format:
/hero_os#w=files:Documents%2FProjects%2Ffoo.md;calendar:2026-04<island_id>:<url_segment>. Segment is URL-encoded.url_segment, only its id appears:shrimp.url_segmentviafrom_url_segment().Island implements (native):
Iframe: iframe includes
url_segmentin itshero:routemessage; on restore, parent sends{type: "hero:restore", segment}after iframe loads.6. Header component
Location:
crates/hero_os_app/src/components/window_header.rs(new). Renders insidewindow.rs, always present (deleteheaderlessbranch).Visual (adopt Files' current style):
Name › Docs › foo.md), last chip bold, earlier chips clickable.window.rs:323-345via.closest('.island-header')— keep the class name for compat).Mobile renderer: same component, different CSS — full-width (no side insets), controls always visible (no hover on touch), breadcrumbs truncate middle (
Files › … › foo.md).7. Back/forward
set()(exceptreplace()).Cmd/Ctrl+[back,Cmd/Ctrl+]forward. Wired inkeyboard.rsashero:keyboard-shortcutevents.8. Migration phases
Each phase is a separate PR, independently shippable.
Phase 1 — foundation (no visible change)
Route,Breadcrumb,HeaderAction,WindowRoutecontext.WindowState.Phase 2 — unified header (visible change: Files-style chrome everywhere)
window_header.rs.window.rs:393-551with single header render.main.rs:1973)..win-header,.win-controlsCSS.Phase 3 — Files proof-of-concept
IslandHeadercall.WindowRouteon navigation.Phase 4 — iframe protocol + shrimp proof-of-concept
hero-window-route.jshelper./hero_agent/uito emithero:route.Phase 5 — URL serialization
RouteSerdetrait, implement for Files + shrimp.hero_os_app.Phase 6 — sweep remaining 15 native islands
IslandHeaderinternal calls, adduse_window_routecalls.Phase 7 — sweep remaining ~24 iframe islands
hero-window-route.jsimport + minimal breadcrumb push.Phase 8 — cleanup
9. Non-goals (deferred)
10. Open during implementation
HeaderAction.icon— inline SVG string, lucide icon id, or island-defined asset path? Propose lucide id with SVG fallback. Lock in Phase 2.Ready for review. On approval, I open PR for Phase 1.
Implementation complete — all 8 phases landed
hero_archipelagos
35-window-route-types— 6 commitshero_os
35-unified-window-chrome— 5 commitsPer-phase summary
hero_archipelagos_coreuse_window_route+ path breadcrumbs + deep-linkpostMessagebridge +hero-window-route.jshelper + shrimp wiringrouting::replace_url+initial_url_segmentIslandHeadernow forwards to WindowRoutewith_window_id(...)Merge order
hero_archipelagos#35-window-route-types→ development[patch]block at the bottom ofhero_os/Cargo.toml; bumphero_archipelagos_coredep revision.hero_os#35-unified-window-chrome→ developmentOutside-scope work in downstream UIs
The iframe protocol is in place host-side. Embedded UIs in other repos opt in by serving
hero-window-route.jsand callingHeroRoute.push({ breadcrumbs, actions })on internal nav. Each service's adoption is a few lines — tracked separately:Until each is updated, those iframes stay headerless in the unified chrome — the OS still shows the island's metadata icon + name as a fallback.
Open UX questions for follow-up PRs
HeaderAction.icon— currently accepts inline SVG; lucide-id support is a small follow-up if preferred.IslandHeadercomponent is now misleadingly named (no longer renders a header); eventual rename / deprecation worth scheduling.