detached a7f39d5

improve(omnibox): harden shadow host against page removal

commit
a7f39d5a06f24e06c08ce5cc08d810325d34703d
author
bamen · · 2025-09-16 20:35 UTC
committer
bamen · · 2025-09-16 16:40 UTC
parent
59edcdf
signature
unsigned
2 files · +57 -13
M CHANGELOG.md +1 -0
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
1414 - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided.
1515 - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits.
1616 - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity.
17+- Omnibox overlay now self-heals if hostile pages remove the injected shadow host, ensuring Ctrl/Cmd+L works everywhere.
1718
1819 ## [0.8.0] - 2025-09-17
1920
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
14 - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided.14 - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided.
15 - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits.15 - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits.
16 - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity.16 - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity.
17+- Omnibox overlay now self-heals if hostile pages remove the injected shadow host, ensuring Ctrl/Cmd+L works everywhere.
17 18
18 ## [0.8.0] - 2025-09-1719 ## [0.8.0] - 2025-09-17
19 20
M gui/src/injected/modules/shadowHost.ts +56 -13
@@ -14,18 +14,7 @@ import globalStyles from '../../styles/tailwind.css?inline';
1414 // Track shadow-root initialization to avoid duplicate reset injection
1515 const shadowResetApplied = new WeakSet<ShadowRoot>();
1616
17-export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root';
18-
19-/**
20- * Ensure and return the global ShadowRoot used by injected UI.
21- */
22-export function getGlobalShadowRoot(): ShadowRoot {
23- // Create or find the host container
24- let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null;
25- if (!host) {
26- host = document.createElement('div');
27- host.id = GLOBAL_SHADOW_HOST_ID;
28- host.style.cssText = `
17+const HOST_STYLES = `
2918 position: fixed !important;
3019 top: 0 !important;
3120 left: 0 !important;
@@ -40,9 +29,63 @@ export function getGlobalShadowRoot(): ShadowRoot {
4029 isolation: isolate !important;
4130 contain: layout style !important;
4231 `;
43- document.documentElement.appendChild(host);
32+
33+let cachedHost: HTMLElement | null = null;
34+let hostObserver: MutationObserver | null = null;
35+let hostKeepaliveTimer: number | null = null;
36+
37+function ensureHostPresence(host: HTMLElement): void {
38+ const docEl = document.documentElement;
39+ if (docEl && !host.isConnected) {
40+ docEl.appendChild(host);
41+ }
42+
43+ if (typeof MutationObserver !== 'undefined') {
44+ if (!hostObserver) {
45+ hostObserver = new MutationObserver(() => {
46+ if (cachedHost && !cachedHost.isConnected && document.documentElement) {
47+ document.documentElement.appendChild(cachedHost);
48+ }
49+ });
50+ }
51+
52+ try {
53+ hostObserver.disconnect();
54+ if (docEl) {
55+ hostObserver.observe(docEl, { childList: true });
56+ }
57+ } catch {
58+ // MutationObserver might not be allowed by the page; fall back to polling in that case.
59+ }
60+ }
61+
62+ if (hostKeepaliveTimer === null && typeof window !== 'undefined') {
63+ hostKeepaliveTimer = window.setInterval(() => {
64+ if (cachedHost && !cachedHost.isConnected && document.documentElement) {
65+ document.documentElement.appendChild(cachedHost);
66+ }
67+ }, 2000);
68+ }
69+}
70+
71+export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root';
72+
73+/**
74+ * Ensure and return the global ShadowRoot used by injected UI.
75+ */
76+export function getGlobalShadowRoot(): ShadowRoot {
77+ // Create or find the host container
78+ let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null;
79+ if (!host) {
80+ host = document.createElement('div');
81+ host.id = GLOBAL_SHADOW_HOST_ID;
4482 }
4583
84+ host.style.cssText = HOST_STYLES;
85+
86+ cachedHost = host;
87+ ensureHostPresence(host);
88+
4689 const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' });
4790
4891 // Reflect current theme from document root to the shadow host so :host(.dark) works
@@ -14,18 +14,7 @@ import globalStyles from '../../styles/tailwind.css?inline';
14 // Track shadow-root initialization to avoid duplicate reset injection14 // Track shadow-root initialization to avoid duplicate reset injection
15 const shadowResetApplied = new WeakSet<ShadowRoot>();15 const shadowResetApplied = new WeakSet<ShadowRoot>();
16 16
17-export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root';17+const HOST_STYLES = `
18-
19-/**
20- * Ensure and return the global ShadowRoot used by injected UI.
21- */
22-export function getGlobalShadowRoot(): ShadowRoot {
23- // Create or find the host container
24- let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null;
25- if (!host) {
26- host = document.createElement('div');
27- host.id = GLOBAL_SHADOW_HOST_ID;
28- host.style.cssText = `
29 position: fixed !important;18 position: fixed !important;
30 top: 0 !important;19 top: 0 !important;
31 left: 0 !important;20 left: 0 !important;
@@ -40,9 +29,63 @@ export function getGlobalShadowRoot(): ShadowRoot {
40 isolation: isolate !important;29 isolation: isolate !important;
41 contain: layout style !important;30 contain: layout style !important;
42 `;31 `;
43- document.documentElement.appendChild(host);32+
33+let cachedHost: HTMLElement | null = null;
34+let hostObserver: MutationObserver | null = null;
35+let hostKeepaliveTimer: number | null = null;
36+
37+function ensureHostPresence(host: HTMLElement): void {
38+ const docEl = document.documentElement;
39+ if (docEl && !host.isConnected) {
40+ docEl.appendChild(host);
41+ }
42+
43+ if (typeof MutationObserver !== 'undefined') {
44+ if (!hostObserver) {
45+ hostObserver = new MutationObserver(() => {
46+ if (cachedHost && !cachedHost.isConnected && document.documentElement) {
47+ document.documentElement.appendChild(cachedHost);
48+ }
49+ });
50+ }
51+
52+ try {
53+ hostObserver.disconnect();
54+ if (docEl) {
55+ hostObserver.observe(docEl, { childList: true });
56+ }
57+ } catch {
58+ // MutationObserver might not be allowed by the page; fall back to polling in that case.
59+ }
60+ }
61+
62+ if (hostKeepaliveTimer === null && typeof window !== 'undefined') {
63+ hostKeepaliveTimer = window.setInterval(() => {
64+ if (cachedHost && !cachedHost.isConnected && document.documentElement) {
65+ document.documentElement.appendChild(cachedHost);
66+ }
67+ }, 2000);
68+ }
69+}
70+
71+export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root';
72+
73+/**
74+ * Ensure and return the global ShadowRoot used by injected UI.
75+ */
76+export function getGlobalShadowRoot(): ShadowRoot {
77+ // Create or find the host container
78+ let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null;
79+ if (!host) {
80+ host = document.createElement('div');
81+ host.id = GLOBAL_SHADOW_HOST_ID;
44 }82 }
45 83
84+ host.style.cssText = HOST_STYLES;
85+
86+ cachedHost = host;
87+ ensureHostPresence(host);
88+
46 const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' });89 const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' });
47 90
48 // Reflect current theme from document root to the shadow host so :host(.dark) works91 // Reflect current theme from document root to the shadow host so :host(.dark) works

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 9

Tags 42

↑↓ moveenter openesc close