Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions internal/ui/live_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ type liveDocumentData struct {
BodyAttrs template.HTMLAttr
}

// wcoBootScript toggles a `wco` class on <html> when the PWA is running with
// Window Controls Overlay so the app can paint its own header into the OS title
// bar. Runs in <head> (before <body> exists) so the class is set on the root
// element with no flash, and tracks runtime changes via geometrychange.
// wcoBootScript runs in <head> before any CSS loads.
// It does two things:
// 1. Sets an inline background-color on <html> from localStorage so the
// correct theme colour is present from the very first paint, eliminating
// the white/gray flash visible in the WCO title-bar area during navigation.
// 2. Toggles the `wco` class when Window Controls Overlay is active.
// wcoBootScript runs in <head> before any CSS loads.
// It does two things:
// 1. Sets an inline background-color on <html> matching the current theme
// and WCO state so the correct colour is present from the very first
// paint, eliminating the white/gray flash in the title-bar area.
// 2. Toggles the `wco` class when Window Controls Overlay is active.
const wcoBootScript = `<script>
(function(){
var chromeBgs = {dark:'#0f0f14',light:'#ddddda',nord:'#292f3a',dracula:'#242631'};
var bodyBgs = {dark:'#111116',light:'#f6f5f2',nord:'#2e3440',dracula:'#282a36'};
var o = navigator.windowControlsOverlay;
function applyBg(){
var t = 'dark';
try{ t = localStorage.getItem('pi-web-theme') || 'dark'; }catch(e){}
var isWCO = o && o.visible;
var map = isWCO ? chromeBgs : bodyBgs;
document.documentElement.style.backgroundColor = map[t] || map.dark;
}
applyBg();
if(!o) return;
function sync(){
document.documentElement.classList.toggle('wco', !!o.visible);
applyBg();
}
sync();
try{ o.addEventListener('geometrychange', sync); }catch(e){}
})();
</script>`

func renderLiveDocumentStart(data liveDocumentData) string {
var b strings.Builder
b.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n")
Expand All @@ -32,6 +71,8 @@ func renderLiveDocumentStart(data liveDocumentData) string {
b.WriteString("<meta name=\"mobile-web-app-capable\" content=\"yes\">\n")
b.WriteString("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n")
b.WriteString("<meta name=\"apple-mobile-web-app-title\" content=\"Pi Sessions\">\n")
b.WriteString(wcoBootScript)
b.WriteByte('\n')
if data.Styles != "" {
b.WriteString(string(data.Styles))
b.WriteByte('\n')
Expand Down Expand Up @@ -76,14 +117,15 @@ func themeBootScript(defaultTheme string) template.HTML {
else if(t === 'custom') icon = '⚙';
document.querySelectorAll('[data-theme-icon]').forEach(function(el){ el.textContent = icon; });
document.querySelectorAll('[data-command-theme-icon]').forEach(function(el){ el.textContent = icon; });
var isWCO = navigator.windowControlsOverlay && navigator.windowControlsOverlay.visible;
var chromeBg = '#0f0f14', bodyBg = '#111116';
if(t === 'light') { chromeBg = '#ddddda'; bodyBg = '#f6f5f2'; }
else if(t === 'nord') { chromeBg = '#292f3a'; bodyBg = '#2e3440'; }
else if(t === 'dracula') { chromeBg = '#242631'; bodyBg = '#282a36'; }
var color = isWCO ? chromeBg : bodyBg;
document.documentElement.style.backgroundColor = color;
var meta = document.querySelector('meta[name="theme-color"]');
if(meta) {
var color = '#111116';
if(t === 'light') color = '#f6f5f2';
else if(t === 'nord') color = '#2e3440';
else if(t === 'dracula') color = '#282a36';
meta.content = color;
}
if(meta) { meta.content = color; }
}
function toggleTheme(){
var idx = themes.indexOf(currentTheme());
Expand Down
1 change: 1 addition & 0 deletions internal/ui/live_templates/assets/manifest.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"start_url": "/",
"scope": "/",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"background_color": "#0e0e13",
"theme_color": "#0e0e13",
"icons": [
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/live_templates/session.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{.LiveDocumentStart}} <script>try{const c=localStorage.getItem('pi-share:v1:sidebar-collapsed');if(c==='true')document.body.classList.add('sidebar-collapsed');}catch(e){}try{const rc=localStorage.getItem('pi-web:v1:right-sidebar-collapsed');if(rc!=='false')document.body.classList.add('right-sidebar-collapsed');}catch(e){}</script>
{{.LiveDocumentStart}} <script>try{const c=localStorage.getItem('pi-share:v1:sidebar-collapsed');if(c==='true')document.body.classList.add('sidebar-collapsed');}catch(e){}try{const rc=localStorage.getItem('pi-web:v1:right-sidebar-collapsed');if(rc==='true')document.body.classList.add('right-sidebar-collapsed');}catch(e){}</script>
{{.ThemeBoot}}
{{.ServiceWorker}}

Expand Down
28 changes: 28 additions & 0 deletions internal/ui/live_templates/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ a { color: inherit; }
margin: 0 auto;
padding: 16px 28px 18px;
}
/* Standalone without WCO: merge header with OS title bar (both use body-bg). */
@media (display-mode: standalone) {
.header {
background: var(--body-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}

/* Window Controls Overlay (installed PWA): make the header act as the OS title
bar — draggable, theme-matched, clearing the window-control inset. */
:root.wco .header {
background: var(--chrome-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
-webkit-app-region: drag;
app-region: drag;
}
:root.wco .header-inner {
padding-left: calc(env(titlebar-area-x, 0px) + 28px);
}
:root.wco .header a,
:root.wco .header button,
:root.wco .header input,
:root.wco .header kbd {
-webkit-app-region: no-drag;
app-region: no-drag;
}
.header-top {
display: flex;
align-items: center;
Expand Down
66 changes: 57 additions & 9 deletions internal/ui/live_templates/styles/session.css
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,11 @@
display: flex;
flex-direction: column;
overflow: hidden;
/* Lift the lit reading column above the darker chrome so it reads as a
raised panel, especially when framed by both sidebars. */
position: relative;
z-index: 1;
box-shadow: 0 0 22px rgba(0, 0, 0, 0.13);
}

/* Main content — scrolls independently within #app's fixed height */
Expand Down Expand Up @@ -1050,6 +1055,41 @@
-webkit-backdrop-filter: blur(16px);
}

/* Standalone PWA without WCO: the OS draws its own title bar above the
page. Make the session header use --body-bg (same as theme-color in
this mode) so the OS strip and our header merge into one seamless zone,
clearly distinct from the WCO compact look. */
@media (display-mode: standalone) {
.session-header-bar {
background: var(--body-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}

/* Window Controls Overlay (installed PWA): draw the header into the OS
title bar. Reserve the inset for the window controls and make the bar
draggable, while keeping interactive children clickable. */
:root.wco .session-header-bar {
/* Stay full-width (base left:0/right:0) so the bottom border spans the
whole window; just inset the content past the OS window controls on
both sides. Background matches theme-color (== --chrome-bg) so the
browser-painted control strip blends seamlessly. */
padding-left: calc(env(titlebar-area-x, 0px) + 18px);
padding-right: calc(100% - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100%) + 18px);
background: var(--chrome-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
-webkit-app-region: drag;
app-region: drag;
}

:root.wco .session-header-bar a,
:root.wco .session-header-bar button {
-webkit-app-region: no-drag;
app-region: no-drag;
}

.session-header-back {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -2193,22 +2233,29 @@
width: 100%;
box-sizing: border-box;
display: block;
padding: 18px 14px calc(14px + env(safe-area-inset-bottom));
background: var(--chrome-bg);
padding: 14px 14px 0;
/* Share the reading column's surface so there's no hard seam between the
message stream and the composer — the shell card below is the only
visible frame. */
background: var(--body-bg);
}

/* Git branch + Create PR row beneath the input box. */
/* Git branch + Create PR row — a full-bleed footer dock anchored to the
bottom of the reading column. The negative margins cancel the composer's
horizontal padding so the chrome surface runs edge to edge, framing the
column from below the way the sidebars frame it from the sides. */
.pi-git-bar {
width: 100%;
max-width: 760px;
margin: 8px auto 0;
width: auto;
margin: 14px -14px 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 2px;
padding: 9px 16px calc(9px + env(safe-area-inset-bottom));
font-size: 11px;
background: var(--chrome-bg);
border-top: 1px solid var(--dim);
}
.pi-git-bar[hidden] { display: none; }

Expand Down Expand Up @@ -2364,14 +2411,15 @@
overflow: visible;
background: var(--input-bg);
border: 1px solid var(--dim);
border-radius: 6px;
border-radius: 10px;
position: relative;
padding: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: border-color 0.15s, box-shadow 0.2s;
}
.pi-chat-shell:focus-within {
border-color: color-mix(in srgb, var(--accent, #8abeb7) 60%, var(--dim));
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.14);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}

.pi-chat-model-popup {
Expand Down
10 changes: 10 additions & 0 deletions internal/ui/live_templates/styles/theme.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
/* ── Unified CSS Variables Theme Engine ── */

/* Smooth cross-fade for MPA navigations (Chrome 126+). Eliminates the
hard-cut flash when moving between the index and session pages. */
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.12s;
}

:root {
--font-sans: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
--palette-surface: var(--surface);
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/session_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func RenderLiveSessionPage(session sessions.Session) string {
Styles: template.HTML(styles),
BodyAttrs: template.HTMLAttr(bodyAttrs),
})),
ThemeBoot: liveThemeBootScript(),
ThemeBoot: themeBootScript("nord"),
ServiceWorker: liveServiceWorkerScript(),
SessionCommandMenu: sessionDesktopMenuHTML(),
MobileCommandMenu: sessionMobileMenuHTML(),
Expand Down
Loading
Loading