On an Android standalone PWA the composer's bottom control row was rendering about one line below the visible viewport, behind the system gesture bar, so those controls were unreachable. The theme already pads the titlebar with env(safe-area-inset-top/left/right), but the viewport meta never opted into viewport-fit=cover, so every safe-area inset collapsed to 0 and the bottom edge had no reservation. Add viewport-fit=cover to the viewport meta (base patch) so the insets carry real values, and reserve env(safe-area-inset-bottom) at the bottom of the composer (brand.css), additive with the app's existing --keyboard-bottom-inset and absorbed by the flex-1 message scroller so total height stays within the viewport. Inert on desktop (env() resolves to 0). Dockerfile verifies the meta patch landed; brand/dockerfile contracts covered by tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
97 lines
4.5 KiB
Python
97 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Apply Atlas baseline UI policy patches to the pinned Hermes WebUI."""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
config = Path("/opt/hermes-webui/api/config.py")
|
|
source = config.read_text(encoding="utf-8")
|
|
before = (
|
|
'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")'
|
|
)
|
|
after = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")'
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI reasoning-effort patch context changed")
|
|
config.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
index = Path("/opt/hermes-webui/static/index.html")
|
|
source = index.read_text(encoding="utf-8")
|
|
before = ' <div class="reasoning-option" data-effort="max">Max</div>\n'
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI xhigh UI patch context changed")
|
|
index.write_text(source.replace(before, "", 1), encoding="utf-8")
|
|
|
|
# Enable edge-to-edge safe-area insets on mobile / standalone PWAs. The theme
|
|
# already pads the titlebar with env(safe-area-inset-top/left/right) and the
|
|
# composer reserves env(safe-area-inset-bottom) (hermes-brand.css), but env()
|
|
# resolves to 0 unless the viewport opts into viewport-fit=cover. Android
|
|
# standalone PWAs render the webview edge-to-edge regardless, so without cover
|
|
# there is no inset to pad with and the composer's control row hides behind the
|
|
# gesture bar. Opt in so those insets carry real values.
|
|
source = index.read_text(encoding="utf-8")
|
|
before = (
|
|
'<meta name="viewport" content="width=device-width, initial-scale=1, '
|
|
'maximum-scale=1, user-scalable=no">'
|
|
)
|
|
after = (
|
|
'<meta name="viewport" content="width=device-width, initial-scale=1, '
|
|
'maximum-scale=1, user-scalable=no, viewport-fit=cover">'
|
|
)
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI viewport-fit patch context changed")
|
|
index.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
# oauth2-proxy returns 401 for browser API and health probes when the secure
|
|
# session expires. Re-enter OIDC with the complete return path.
|
|
ui = Path("/opt/hermes-webui/static/ui.js")
|
|
source = ui.read_text(encoding="utf-8")
|
|
before = """ const res=await fetcher(_offlineHealthUrl(),opts);
|
|
return !!(res&&res.ok);
|
|
"""
|
|
after = """ const res=await fetcher(_offlineHealthUrl(),opts);
|
|
if(res&&(res.status===401||res.status===403)){
|
|
const rd=window.location.pathname+window.location.search+window.location.hash;
|
|
window.location.assign('/oauth2/start?rd='+encodeURIComponent(rd));
|
|
return false;
|
|
}
|
|
return !!(res&&res.ok);
|
|
"""
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI auth-recovery patch context changed")
|
|
ui.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
# Make delegated session hierarchy obvious and collapsible in the sidebar.
|
|
sessions = Path("/opt/hermes-webui/static/sessions.js")
|
|
source = sessions.read_text(encoding="utf-8")
|
|
before = """ const childLabel=t('session_meta_children', childCount);
|
|
childCountEl.textContent=childLabel;
|
|
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
|
|
"""
|
|
after = """ const childLabel=t('session_meta_children', childCount);
|
|
const childrenExpanded=_expandedChildSessionKeys.has(lineageKey)||!!searchQueryRaw;
|
|
childCountEl.textContent=(childrenExpanded?'▾ ':'▸ ')+childLabel;
|
|
childCountEl.setAttribute('aria-expanded',childrenExpanded?'true':'false');
|
|
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
|
|
"""
|
|
if source.count(before) != 1:
|
|
raise SystemExit("Hermes WebUI child-session toggle patch context changed")
|
|
sessions.write_text(source.replace(before, after, 1), encoding="utf-8")
|
|
|
|
# A profile's model is only its default; label that scope in both render paths.
|
|
panels = Path("/opt/hermes-webui/static/panels.js")
|
|
source = panels.read_text(encoding="utf-8")
|
|
before = " if (typeof p.model === 'string' && p.model) meta.push(p.model.split('/').pop());\n"
|
|
after = """ if (typeof p.model === 'string' && p.model) {
|
|
const routeLabels = {
|
|
'atlas/auto/fast': 'Automatic · Fast',
|
|
'atlas/auto/balanced': 'Automatic · Balanced',
|
|
'atlas/auto/deep': 'Automatic · Deep',
|
|
'atlas/auto/maximum': 'Automatic · Maximum',
|
|
};
|
|
meta.push('profile default: ' + (routeLabels[p.model] || p.model.split('/').pop()));
|
|
}
|
|
"""
|
|
if source.count(before) != 2:
|
|
raise SystemExit("Hermes WebUI profile-model label patch context changed")
|
|
panels.write_text(source.replace(before, after, 2), encoding="utf-8")
|