From 9f16f6e14301d28202b37e2a846626ca02bed196 Mon Sep 17 00:00:00 2001 From: setkyar Date: Mon, 1 Jun 2026 00:09:33 +0700 Subject: [PATCH 1/5] style(composer): unify chat composer surface with message stream Drop the darker --chrome-bg band behind the composer in favor of --body-bg so the message stream and composer share one surface with no hard seam. The input shell becomes the only visible frame: softer 10px radius and a faint resting shadow. --- internal/ui/live_templates/styles/session.css | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/ui/live_templates/styles/session.css b/internal/ui/live_templates/styles/session.css index 3bf0b2e..509aa39 100644 --- a/internal/ui/live_templates/styles/session.css +++ b/internal/ui/live_templates/styles/session.css @@ -2193,8 +2193,11 @@ 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 calc(14px + env(safe-area-inset-bottom)); + /* 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. */ @@ -2364,14 +2367,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 { From bd817361632224d0451ebd9328249b35898b1194 Mon Sep 17 00:00:00 2001 From: setkyar Date: Mon, 1 Jun 2026 10:46:28 +0700 Subject: [PATCH 2/5] style(composer): add footer dock and lift reading column Turn the git branch + PR row into a full-bleed footer dock with a chrome surface and top border anchored to the bottom of the reading column. Add a soft elevation to the content column so it reads as a raised panel against the darker chrome, especially when framed by both sidebars. --- internal/ui/live_templates/styles/session.css | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/internal/ui/live_templates/styles/session.css b/internal/ui/live_templates/styles/session.css index 509aa39..683f3c1 100644 --- a/internal/ui/live_templates/styles/session.css +++ b/internal/ui/live_templates/styles/session.css @@ -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 */ @@ -2193,25 +2198,29 @@ width: 100%; box-sizing: border-box; display: block; - padding: 14px 14px calc(14px + env(safe-area-inset-bottom)); + 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; } From 8170068f39d6901897bf68de8c6bedf4d5ff98f8 Mon Sep 17 00:00:00 2001 From: setkyar Date: Mon, 1 Jun 2026 13:39:46 +0700 Subject: [PATCH 3/5] feat(pwa): window controls overlay + native title bar theming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add display_override: [window-controls-overlay, standalone] to manifest so installed PWA defaults to WCO compact mode on Chrome/Edge - Draw session and index headers into the OS title bar via env(titlebar-area-*) with full-width border-bottom and content inset past traffic lights / Chrome buttons on both sides - Match Chrome's control strip (left/right of our header) by setting theme-color and documentElement.style.backgroundColor to chrome-bg in WCO mode and body-bg in standalone mode — eliminates the colour seam - Use @media (display-mode: standalone) to give standalone-without-WCO a distinct solid body-bg header that merges with the OS title bar - Inline background-color set in before CSS loads to prevent white/grey flash during navigation; updates on theme switch and WCO geometry changes - Add @view-transition { navigation: auto } to theme.css for smooth 120ms cross-fade between index and session pages (Chrome 126+) --- internal/ui/live_page.go | 56 ++++++++++++++++--- .../assets/manifest.webmanifest | 1 + internal/ui/live_templates/styles/index.css | 28 ++++++++++ internal/ui/live_templates/styles/session.css | 35 ++++++++++++ internal/ui/live_templates/styles/theme.css | 10 ++++ 5 files changed, 123 insertions(+), 7 deletions(-) diff --git a/internal/ui/live_page.go b/internal/ui/live_page.go index 71c354e..b9fa218 100644 --- a/internal/ui/live_page.go +++ b/internal/ui/live_page.go @@ -13,6 +13,45 @@ type liveDocumentData struct { BodyAttrs template.HTMLAttr } +// wcoBootScript toggles a `wco` class on when the PWA is running with +// Window Controls Overlay so the app can paint its own header into the OS title +// bar. Runs in (before exists) so the class is set on the root +// element with no flash, and tracks runtime changes via geometrychange. +// wcoBootScript runs in before any CSS loads. +// It does two things: +// 1. Sets an inline background-color on 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 before any CSS loads. +// It does two things: +// 1. Sets an inline background-color on 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 = `` + func renderLiveDocumentStart(data liveDocumentData) string { var b strings.Builder b.WriteString("\n\n\n") @@ -32,6 +71,8 @@ func renderLiveDocumentStart(data liveDocumentData) string { b.WriteString("\n") b.WriteString("\n") b.WriteString("\n") + b.WriteString(wcoBootScript) + b.WriteByte('\n') if data.Styles != "" { b.WriteString(string(data.Styles)) b.WriteByte('\n') @@ -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()); diff --git a/internal/ui/live_templates/assets/manifest.webmanifest b/internal/ui/live_templates/assets/manifest.webmanifest index dbcc276..14b032b 100644 --- a/internal/ui/live_templates/assets/manifest.webmanifest +++ b/internal/ui/live_templates/assets/manifest.webmanifest @@ -5,6 +5,7 @@ "start_url": "/", "scope": "/", "display": "standalone", + "display_override": ["window-controls-overlay", "standalone"], "background_color": "#0e0e13", "theme_color": "#0e0e13", "icons": [ diff --git a/internal/ui/live_templates/styles/index.css b/internal/ui/live_templates/styles/index.css index b324203..f5f657f 100644 --- a/internal/ui/live_templates/styles/index.css +++ b/internal/ui/live_templates/styles/index.css @@ -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; diff --git a/internal/ui/live_templates/styles/session.css b/internal/ui/live_templates/styles/session.css index 683f3c1..262483f 100644 --- a/internal/ui/live_templates/styles/session.css +++ b/internal/ui/live_templates/styles/session.css @@ -1055,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; diff --git a/internal/ui/live_templates/styles/theme.css b/internal/ui/live_templates/styles/theme.css index 72a09ef..95df894 100644 --- a/internal/ui/live_templates/styles/theme.css +++ b/internal/ui/live_templates/styles/theme.css @@ -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); From 61f1311f8b718eb1b6445707d69d77daef4e02ce Mon Sep 17 00:00:00 2001 From: setkyar Date: Mon, 1 Jun 2026 14:35:49 +0700 Subject: [PATCH 4/5] feat(session): default to nord theme and show right sidebar Detail page now defaults to the nord theme and shows the scratchpad sidebar by default. Existing user preferences in localStorage still win. --- internal/ui/live_templates/session.html | 2 +- internal/ui/session_page.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ui/live_templates/session.html b/internal/ui/live_templates/session.html index 4932e16..7e10840 100644 --- a/internal/ui/live_templates/session.html +++ b/internal/ui/live_templates/session.html @@ -1,4 +1,4 @@ -{{.LiveDocumentStart}} +{{.LiveDocumentStart}} {{.ThemeBoot}} {{.ServiceWorker}} diff --git a/internal/ui/session_page.go b/internal/ui/session_page.go index 9ccf6e9..948d551 100644 --- a/internal/ui/session_page.go +++ b/internal/ui/session_page.go @@ -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(), From 88d46a100b2b82e60c76a635c493afba1f4b47fd Mon Sep 17 00:00:00 2001 From: setkyar Date: Mon, 1 Jun 2026 14:39:00 +0700 Subject: [PATCH 5/5] chore(deps): drop optional terser dependency subtree from lockfile --- web/package-lock.json | 130 ------------------------------------------ 1 file changed, 130 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index b501f4c..2160af2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -271,44 +271,6 @@ } } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -316,19 +278,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -778,21 +727,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -813,15 +747,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -832,15 +757,6 @@ "node": ">=18" } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1509,18 +1425,6 @@ "dev": true, "license": "ISC" }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1531,19 +1435,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -1565,27 +1456,6 @@ "dev": true, "license": "MIT" }, - "node_modules/terser": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.0.tgz", - "integrity": "sha512-TV+JFkQFtljk12ffyYAA4+zVF4Hs+qaROsT+Qo9o2/z39x+IUn+pvsmomiCPlp5YigfR1OdbGHOvc0L+Ca1X7g==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",