fix: keep PWA chrome visible after resume

This commit is contained in:
Federico Jaramillo Martinez
2026-05-28 08:19:04 +02:00
parent f569467769
commit c73ac5bb9a
3 changed files with 62 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Keep PWA navigation bars visible after returning to the app from the background.
+56 -1
View File
@@ -51,6 +51,7 @@ const THEME_AUTO_OFF_VALUE = "auto:off";
const THEME_OPTION_PREFIX = "theme:";
const TERMINAL_ROUTE_NAMESPACE = queryNamespace("core:workspace.terminal");
const REFRESH_LONG_PRESS_MS = 550;
const VIEWPORT_POSITION_REPAIR_DELAY_MS = 250;
@customElement("pi-web-app")
export class PiWebApp extends LitElement {
@@ -119,6 +120,8 @@ export class PiWebApp extends LitElement {
private themePreference: ThemePreference = readStoredThemePreference() ?? DEFAULT_THEME_PREFERENCE;
private refreshLongPressTimer: number | undefined;
private suppressNextRefreshClick = false;
private viewportPositionRepairFrame: number | undefined;
private viewportPositionRepairTimer: number | undefined;
@state() private activeThemeId: QualifiedContributionId = CLASSIC_THEME_ID;
@state() private isMobileNavigationLayout = this.mobileNavigationMedia?.matches ?? false;
@state() private isPwaDisplayMode = detectPwaDisplayMode(this.pwaDisplayModeMedia);
@@ -131,7 +134,11 @@ export class PiWebApp extends LitElement {
@state() private mobileTabsCanScrollLeft = false;
@state() private mobileTabsCanScrollRight = false;
private readonly onPopState = () => void this.withChatScrollTransition(() => this.restoreRoute(false));
private readonly onPageShow = () => {
this.repairViewportPosition();
};
private readonly onFocus = () => {
this.repairViewportPosition();
void this.sessions.refreshSelectedSession();
void this.refreshPiWebStatus();
void this.refreshWorkspaceActivity();
@@ -139,6 +146,7 @@ export class PiWebApp extends LitElement {
};
private readonly onVisibilityChange = () => {
if (document.visibilityState === "visible") {
this.repairViewportPosition();
void this.sessions.refreshSelectedSession();
void this.refreshPiWebStatus();
void this.refreshWorkspaceActivity();
@@ -185,6 +193,7 @@ export class PiWebApp extends LitElement {
override connectedCallback(): void {
super.connectedCallback();
window.addEventListener("popstate", this.onPopState);
window.addEventListener("pageshow", this.onPageShow);
window.addEventListener("focus", this.onFocus);
document.addEventListener("click", this.onDocumentClick);
document.addEventListener("visibilitychange", this.onVisibilityChange);
@@ -203,6 +212,7 @@ export class PiWebApp extends LitElement {
override disconnectedCallback(): void {
window.removeEventListener("popstate", this.onPopState);
window.removeEventListener("pageshow", this.onPageShow);
window.removeEventListener("focus", this.onFocus);
document.removeEventListener("click", this.onDocumentClick);
document.removeEventListener("visibilitychange", this.onVisibilityChange);
@@ -226,6 +236,7 @@ export class PiWebApp extends LitElement {
this.mobileTabsResizeObserver = undefined;
this.observedMobileTabs = undefined;
this.clearRefreshLongPressTimer();
this.clearViewportPositionRepair();
super.disconnectedCallback();
}
@@ -353,7 +364,51 @@ export class PiWebApp extends LitElement {
await this.chatView?.updateComplete;
await nextFrame();
this.chatView?.restoreScrollPosition();
this.promptEditor?.focusInput();
if (this.shouldAutoFocusPrompt()) this.promptEditor?.focusInput();
}
private shouldAutoFocusPrompt(): boolean {
return !this.isMobileNavigationLayout && !this.isPwaDisplayMode;
}
private repairViewportPosition(): void {
if (!this.shouldRepairViewportPosition()) return;
this.resetViewportScroll();
if (this.viewportPositionRepairFrame !== undefined) window.cancelAnimationFrame(this.viewportPositionRepairFrame);
this.viewportPositionRepairFrame = window.requestAnimationFrame(() => {
this.viewportPositionRepairFrame = undefined;
this.resetViewportScroll();
this.viewportPositionRepairFrame = window.requestAnimationFrame(() => {
this.viewportPositionRepairFrame = undefined;
this.resetViewportScroll();
});
});
if (this.viewportPositionRepairTimer !== undefined) window.clearTimeout(this.viewportPositionRepairTimer);
this.viewportPositionRepairTimer = window.setTimeout(() => {
this.viewportPositionRepairTimer = undefined;
this.resetViewportScroll();
}, VIEWPORT_POSITION_REPAIR_DELAY_MS);
}
private shouldRepairViewportPosition(): boolean {
return this.isMobileNavigationLayout || this.isPwaDisplayMode;
}
private resetViewportScroll(): void {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}
private clearViewportPositionRepair(): void {
if (this.viewportPositionRepairFrame !== undefined) {
window.cancelAnimationFrame(this.viewportPositionRepairFrame);
this.viewportPositionRepairFrame = undefined;
}
if (this.viewportPositionRepairTimer !== undefined) {
window.clearTimeout(this.viewportPositionRepairTimer);
this.viewportPositionRepairTimer = undefined;
}
}
private async withChatPrependTransition(action: () => Promise<void>) {
+1 -1
View File
@@ -50,7 +50,7 @@ export interface CompletionItem {
}
export const appStyles = css`
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; }
:host { position: fixed; top: 0; right: 0; left: 0; display: block; height: 100dvh; box-sizing: border-box; overflow: hidden; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; }
.shell { display: grid; grid-template-columns: 340px minmax(420px, 1fr) minmax(360px, 42vw); height: 100%; min-height: 0; }
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--pi-border); overflow: hidden; }
header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); }