detached c21700d

refactor(gui): remove JavaScript window.open interception

Remove ~280 lines of window.open interception code:
- WindowOpenDebouncer class and deduplication logic
- User interaction tracking
- Fake window proxy creation
- WindowIntent message passing

JavaScript no longer intercepts window.open - WebKit's native
create signal handles it properly at the browser layer.
commit
c21700deb0f3fed87710151485f4b8cc9f8d4020
author
bamen · · 2025-10-12 07:05 UTC
parent
dbdc6c0
signature
unsigned
1 file · +0 -320
M gui/src/injected/main-world.ts +0 -320
@@ -14,7 +14,6 @@ declare global {
1414 interface Window {
1515 __dumber: DumberAPI;
1616 __dumber_page_bridge_installed?: boolean;
17- __dumber_window_open_intercepted?: boolean;
1817 __dumber_dom_zoom_seed?: number;
1918 __dumber_dom_zoom_level?: number;
2019 __dumber_initial_theme?: string;
@@ -46,16 +45,6 @@ declare global {
4645 }
4746 }
4847
49-interface WindowIntent {
50- url: string;
51- target: string;
52- features: string;
53- timestamp: number;
54- userTriggered: boolean;
55- requestId: string;
56- windowType: string; // "tab", "popup", "unknown"
57-}
58-
5948 interface Suggestion {
6049 // Define proper suggestion types based on your needs
6150 [key: string]: unknown;
@@ -75,36 +64,6 @@ interface DumberAPI {
7564 };
7665 }
7766
78-// Function to detect if window.open call is for a popup based on features
79-function detectWindowType(features?: string | null): string {
80- if (!features) {
81- return "tab"; // Default to tab if no features specified
82- }
83-
84- const featuresStr = features.toLowerCase();
85-
86- // Check for typical popup characteristics
87- const hasSize = /width=\d+|height=\d+/.test(featuresStr);
88- const hasNoToolbar = /toolbar=0|toolbar=no/.test(featuresStr);
89- const hasNoMenubar = /menubar=0|menubar=no/.test(featuresStr);
90- const hasNoLocation = /location=0|location=no/.test(featuresStr);
91-
92- // OAuth/login popups typically have size constraints and disabled UI elements
93- const popupIndicators = [
94- hasSize,
95- hasNoToolbar,
96- hasNoMenubar,
97- hasNoLocation,
98- ].filter(Boolean).length;
99-
100- // If 2 or more popup indicators are present, treat as popup
101- if (popupIndicators >= 2) {
102- return "popup";
103- }
104-
105- return "tab";
106-}
107-
10867 (() => {
10968 try {
11069 // Prevent multiple installations
@@ -899,284 +858,6 @@ function detectWindowType(features?: string | null): string {
899858 // Setup OAuth callback detection
900859 setupOAuthCallbackDetection();
901860
902- // Enhanced window.open deduplication system
903- interface PendingRequest {
904- id: string;
905- url: string;
906- timestamp: number;
907- webviewId: string;
908- resolved: boolean;
909- }
910-
911- class WindowOpenDebouncer {
912- private pendingRequests = new Map<string, PendingRequest>();
913- private readonly DEBOUNCE_WINDOW = 150; // Increased from 100ms
914- private readonly CLEANUP_INTERVAL = 1000; // Clean old entries every 1s
915-
916- generateRequestId(): string {
917- return `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
918- }
919-
920- isDuplicate(url: string, webviewId: string): boolean {
921- const key = this.createKey(url, webviewId);
922- const existing = this.pendingRequests.get(key);
923-
924- if (!existing) return false;
925-
926- const timeDiff = Date.now() - existing.timestamp;
927- return timeDiff < this.DEBOUNCE_WINDOW && !existing.resolved;
928- }
929-
930- registerRequest(url: string, webviewId: string): string {
931- const requestId = this.generateRequestId();
932- const key = this.createKey(url, webviewId);
933-
934- this.pendingRequests.set(key, {
935- id: requestId,
936- url,
937- timestamp: Date.now(),
938- webviewId,
939- resolved: false,
940- });
941-
942- // Schedule cleanup
943- setTimeout(() => this.markResolved(key), this.DEBOUNCE_WINDOW);
944- return requestId;
945- }
946-
947- private createKey(url: string, webviewId: string): string {
948- return `${webviewId}:${url}`;
949- }
950-
951- private markResolved(key: string): void {
952- const request = this.pendingRequests.get(key);
953- if (request) {
954- request.resolved = true;
955- // Clean up after additional delay
956- setTimeout(
957- () => this.pendingRequests.delete(key),
958- this.CLEANUP_INTERVAL,
959- );
960- }
961- }
962- }
963-
964- // Global debouncer instance
965- const windowOpenDebouncer = new WindowOpenDebouncer();
966-
967- // User interaction tracking
968- let lastUserInteraction = 0;
969- const INTERACTION_TIMEOUT = 5000; // 5 seconds
970-
971- const trackUserInteraction = () => {
972- lastUserInteraction = Date.now();
973- };
974-
975- const isUserInteraction = (): boolean => {
976- const now = Date.now();
977- const timeSinceInteraction = now - lastUserInteraction;
978- return timeSinceInteraction <= INTERACTION_TIMEOUT;
979- };
980-
981- const createFakeWindow = (url: string, popupId?: string): WindowProxy => {
982- // Generate unique popup ID if not provided
983- const actualPopupId =
984- popupId ||
985- `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
986-
987- // Set up communication channel via shared localStorage
988- try {
989- localStorage.setItem(
990- `popup_${actualPopupId}_parent_info`,
991- JSON.stringify({
992- parentUrl: window.location.href,
993- parentWebViewId: window.__dumber_webview_id,
994- timestamp: Date.now(),
995- popupUrl: url,
996- }),
997- );
998-
999- console.log(
1000- `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`,
1001- );
1002- } catch (err) {
1003- console.warn(
1004- `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`,
1005- err,
1006- );
1007- }
1008-
1009- return {
1010- closed: false,
1011- location: { href: url },
1012- close() {
1013- (this as { closed: boolean }).closed = true;
1014- },
1015- focus: () => {
1016- try {
1017- localStorage.setItem(
1018- `popup_${actualPopupId}_parent_action`,
1019- JSON.stringify({
1020- action: "focus",
1021- timestamp: Date.now(),
1022- }),
1023- );
1024- } catch (err) {
1025- console.warn(`[window-open] Failed to store focus action:`, err);
1026- }
1027- },
1028- blur: () => {
1029- try {
1030- localStorage.setItem(
1031- `popup_${actualPopupId}_parent_action`,
1032- JSON.stringify({
1033- action: "blur",
1034- timestamp: Date.now(),
1035- }),
1036- );
1037- } catch (err) {
1038- console.warn(`[window-open] Failed to store blur action:`, err);
1039- }
1040- },
1041- postMessage: (data: unknown, targetOrigin?: string) => {
1042- try {
1043- localStorage.setItem(
1044- `popup_${actualPopupId}_message_to_popup`,
1045- JSON.stringify({
1046- data,
1047- origin: targetOrigin || "*",
1048- timestamp: Date.now(),
1049- source: "parent",
1050- }),
1051- );
1052- console.log(
1053- `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`,
1054- data,
1055- );
1056- } catch (err) {
1057- console.warn(`[window-open] Failed to store postMessage:`, err);
1058- }
1059- },
1060- } as unknown as WindowProxy;
1061- };
1062-
1063- // Track user interactions for popup validation
1064- const interactionEvents = [
1065- "click",
1066- "mousedown",
1067- "keydown",
1068- "touchstart",
1069- ] as const;
1070- interactionEvents.forEach((eventType) => {
1071- document.addEventListener(eventType, trackUserInteraction, true);
1072- });
1073- cleanupHandlers.push(() => {
1074- interactionEvents.forEach((eventType) => {
1075- document.removeEventListener(eventType, trackUserInteraction, true);
1076- });
1077- });
1078-
1079- console.log(
1080- `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`,
1081- );
1082-
1083- // Window.open interceptor - only install once
1084- if (!window.__dumber_window_open_intercepted) {
1085- try {
1086- window.open = function (
1087- url?: string | URL | null,
1088- target?: string | null,
1089- features?: string | null,
1090- ): WindowProxy | null {
1091- const urlString = url
1092- ? typeof url === "string"
1093- ? url
1094- : url.toString()
1095- : "";
1096- const webviewId = window.__dumber_webview_id || "unknown";
1097-
1098- console.log(
1099- `[window-open] [WebView ${webviewId}] Bridge called with:`,
1100- urlString,
1101- target,
1102- features,
1103- );
1104-
1105- // Enhanced duplicate check
1106- if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) {
1107- console.log(
1108- `[window-open] [WebView ${webviewId}] BLOCKED: Duplicate request within debounce window`,
1109- );
1110- return createFakeWindow(urlString);
1111- }
1112-
1113- // Active WebView check
1114- if (!window.__dumber_is_active) {
1115- console.log(
1116- `[window-open] [WebView ${webviewId}] BLOCKED: Inactive WebView`,
1117- );
1118- return createFakeWindow(urlString);
1119- }
1120-
1121- // User interaction check
1122- if (!isUserInteraction()) {
1123- console.log(
1124- `[window-open] [WebView ${webviewId}] BLOCKED: No recent user interaction`,
1125- );
1126- return createFakeWindow("");
1127- }
1128-
1129- // Register request and get unique ID
1130- const requestId = windowOpenDebouncer.registerRequest(
1131- urlString,
1132- webviewId,
1133- );
1134-
1135- const intent: WindowIntent = {
1136- url: urlString,
1137- target: target || "_blank",
1138- features: features || "",
1139- timestamp: Date.now(),
1140- userTriggered: true,
1141- requestId, // NEW: Add unique request ID
1142- windowType: detectWindowType(features), // Detect popup vs tab
1143- };
1144-
1145- // Send to Go backend
1146- try {
1147- if (window.webkit?.messageHandlers?.dumber) {
1148- window.webkit.messageHandlers.dumber.postMessage(
1149- JSON.stringify({
1150- type: "handle-window-open",
1151- payload: intent,
1152- }),
1153- );
1154- console.log(
1155- `[window-open] [WebView ${webviewId}] Sent request ${requestId} to Go backend`,
1156- );
1157- }
1158- } catch (err) {
1159- console.error(
1160- `[window-open] [WebView ${webviewId}] Failed to send request ${requestId}:`,
1161- err,
1162- );
1163- }
1164-
1165- return createFakeWindow(urlString, requestId);
1166- };
1167-
1168- window.__dumber_window_open_intercepted = true;
1169- console.log(
1170- `[window-open] [WebView ${window.__dumber_webview_id}] ✅ Page-world bridge installed`,
1171- );
1172- } catch (error) {
1173- console.error(
1174- `[window-open] [WebView ${window.__dumber_webview_id}] ❌ Failed to install page-world bridge:`,
1175- error,
1176- );
1177- }
1178- }
1179-
1180861 const teardown = () => {
1181862 if (teardownExecuted) {
1182863 return;
@@ -1198,7 +879,6 @@ function detectWindowType(features?: string | null): string {
1198879 omniboxQueue.length = 0;
1199880 omniboxReady = false;
1200881
1201- window.__dumber_window_open_intercepted = false;
1202882 window.__dumber_page_bridge_installed = false;
1203883 window.__dumber_teardown = undefined;
1204884 window.__dumber_omnibox_suggestions = undefined;
@@ -14,7 +14,6 @@ declare global {
14 interface Window {14 interface Window {
15 __dumber: DumberAPI;15 __dumber: DumberAPI;
16 __dumber_page_bridge_installed?: boolean;16 __dumber_page_bridge_installed?: boolean;
17- __dumber_window_open_intercepted?: boolean;
18 __dumber_dom_zoom_seed?: number;17 __dumber_dom_zoom_seed?: number;
19 __dumber_dom_zoom_level?: number;18 __dumber_dom_zoom_level?: number;
20 __dumber_initial_theme?: string;19 __dumber_initial_theme?: string;
@@ -46,16 +45,6 @@ declare global {
46 }45 }
47 }46 }
48 47
49-interface WindowIntent {
50- url: string;
51- target: string;
52- features: string;
53- timestamp: number;
54- userTriggered: boolean;
55- requestId: string;
56- windowType: string; // "tab", "popup", "unknown"
57-}
58-
59 interface Suggestion {48 interface Suggestion {
60 // Define proper suggestion types based on your needs49 // Define proper suggestion types based on your needs
61 [key: string]: unknown;50 [key: string]: unknown;
@@ -75,36 +64,6 @@ interface DumberAPI {
75 };64 };
76 }65 }
77 66
78-// Function to detect if window.open call is for a popup based on features
79-function detectWindowType(features?: string | null): string {
80- if (!features) {
81- return "tab"; // Default to tab if no features specified
82- }
83-
84- const featuresStr = features.toLowerCase();
85-
86- // Check for typical popup characteristics
87- const hasSize = /width=\d+|height=\d+/.test(featuresStr);
88- const hasNoToolbar = /toolbar=0|toolbar=no/.test(featuresStr);
89- const hasNoMenubar = /menubar=0|menubar=no/.test(featuresStr);
90- const hasNoLocation = /location=0|location=no/.test(featuresStr);
91-
92- // OAuth/login popups typically have size constraints and disabled UI elements
93- const popupIndicators = [
94- hasSize,
95- hasNoToolbar,
96- hasNoMenubar,
97- hasNoLocation,
98- ].filter(Boolean).length;
99-
100- // If 2 or more popup indicators are present, treat as popup
101- if (popupIndicators >= 2) {
102- return "popup";
103- }
104-
105- return "tab";
106-}
107-
108 (() => {67 (() => {
109 try {68 try {
110 // Prevent multiple installations69 // Prevent multiple installations
@@ -899,284 +858,6 @@ function detectWindowType(features?: string | null): string {
899 // Setup OAuth callback detection858 // Setup OAuth callback detection
900 setupOAuthCallbackDetection();859 setupOAuthCallbackDetection();
901 860
902- // Enhanced window.open deduplication system
903- interface PendingRequest {
904- id: string;
905- url: string;
906- timestamp: number;
907- webviewId: string;
908- resolved: boolean;
909- }
910-
911- class WindowOpenDebouncer {
912- private pendingRequests = new Map<string, PendingRequest>();
913- private readonly DEBOUNCE_WINDOW = 150; // Increased from 100ms
914- private readonly CLEANUP_INTERVAL = 1000; // Clean old entries every 1s
915-
916- generateRequestId(): string {
917- return `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
918- }
919-
920- isDuplicate(url: string, webviewId: string): boolean {
921- const key = this.createKey(url, webviewId);
922- const existing = this.pendingRequests.get(key);
923-
924- if (!existing) return false;
925-
926- const timeDiff = Date.now() - existing.timestamp;
927- return timeDiff < this.DEBOUNCE_WINDOW && !existing.resolved;
928- }
929-
930- registerRequest(url: string, webviewId: string): string {
931- const requestId = this.generateRequestId();
932- const key = this.createKey(url, webviewId);
933-
934- this.pendingRequests.set(key, {
935- id: requestId,
936- url,
937- timestamp: Date.now(),
938- webviewId,
939- resolved: false,
940- });
941-
942- // Schedule cleanup
943- setTimeout(() => this.markResolved(key), this.DEBOUNCE_WINDOW);
944- return requestId;
945- }
946-
947- private createKey(url: string, webviewId: string): string {
948- return `${webviewId}:${url}`;
949- }
950-
951- private markResolved(key: string): void {
952- const request = this.pendingRequests.get(key);
953- if (request) {
954- request.resolved = true;
955- // Clean up after additional delay
956- setTimeout(
957- () => this.pendingRequests.delete(key),
958- this.CLEANUP_INTERVAL,
959- );
960- }
961- }
962- }
963-
964- // Global debouncer instance
965- const windowOpenDebouncer = new WindowOpenDebouncer();
966-
967- // User interaction tracking
968- let lastUserInteraction = 0;
969- const INTERACTION_TIMEOUT = 5000; // 5 seconds
970-
971- const trackUserInteraction = () => {
972- lastUserInteraction = Date.now();
973- };
974-
975- const isUserInteraction = (): boolean => {
976- const now = Date.now();
977- const timeSinceInteraction = now - lastUserInteraction;
978- return timeSinceInteraction <= INTERACTION_TIMEOUT;
979- };
980-
981- const createFakeWindow = (url: string, popupId?: string): WindowProxy => {
982- // Generate unique popup ID if not provided
983- const actualPopupId =
984- popupId ||
985- `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
986-
987- // Set up communication channel via shared localStorage
988- try {
989- localStorage.setItem(
990- `popup_${actualPopupId}_parent_info`,
991- JSON.stringify({
992- parentUrl: window.location.href,
993- parentWebViewId: window.__dumber_webview_id,
994- timestamp: Date.now(),
995- popupUrl: url,
996- }),
997- );
998-
999- console.log(
1000- `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`,
1001- );
1002- } catch (err) {
1003- console.warn(
1004- `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`,
1005- err,
1006- );
1007- }
1008-
1009- return {
1010- closed: false,
1011- location: { href: url },
1012- close() {
1013- (this as { closed: boolean }).closed = true;
1014- },
1015- focus: () => {
1016- try {
1017- localStorage.setItem(
1018- `popup_${actualPopupId}_parent_action`,
1019- JSON.stringify({
1020- action: "focus",
1021- timestamp: Date.now(),
1022- }),
1023- );
1024- } catch (err) {
1025- console.warn(`[window-open] Failed to store focus action:`, err);
1026- }
1027- },
1028- blur: () => {
1029- try {
1030- localStorage.setItem(
1031- `popup_${actualPopupId}_parent_action`,
1032- JSON.stringify({
1033- action: "blur",
1034- timestamp: Date.now(),
1035- }),
1036- );
1037- } catch (err) {
1038- console.warn(`[window-open] Failed to store blur action:`, err);
1039- }
1040- },
1041- postMessage: (data: unknown, targetOrigin?: string) => {
1042- try {
1043- localStorage.setItem(
1044- `popup_${actualPopupId}_message_to_popup`,
1045- JSON.stringify({
1046- data,
1047- origin: targetOrigin || "*",
1048- timestamp: Date.now(),
1049- source: "parent",
1050- }),
1051- );
1052- console.log(
1053- `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`,
1054- data,
1055- );
1056- } catch (err) {
1057- console.warn(`[window-open] Failed to store postMessage:`, err);
1058- }
1059- },
1060- } as unknown as WindowProxy;
1061- };
1062-
1063- // Track user interactions for popup validation
1064- const interactionEvents = [
1065- "click",
1066- "mousedown",
1067- "keydown",
1068- "touchstart",
1069- ] as const;
1070- interactionEvents.forEach((eventType) => {
1071- document.addEventListener(eventType, trackUserInteraction, true);
1072- });
1073- cleanupHandlers.push(() => {
1074- interactionEvents.forEach((eventType) => {
1075- document.removeEventListener(eventType, trackUserInteraction, true);
1076- });
1077- });
1078-
1079- console.log(
1080- `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`,
1081- );
1082-
1083- // Window.open interceptor - only install once
1084- if (!window.__dumber_window_open_intercepted) {
1085- try {
1086- window.open = function (
1087- url?: string | URL | null,
1088- target?: string | null,
1089- features?: string | null,
1090- ): WindowProxy | null {
1091- const urlString = url
1092- ? typeof url === "string"
1093- ? url
1094- : url.toString()
1095- : "";
1096- const webviewId = window.__dumber_webview_id || "unknown";
1097-
1098- console.log(
1099- `[window-open] [WebView ${webviewId}] Bridge called with:`,
1100- urlString,
1101- target,
1102- features,
1103- );
1104-
1105- // Enhanced duplicate check
1106- if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) {
1107- console.log(
1108- `[window-open] [WebView ${webviewId}] BLOCKED: Duplicate request within debounce window`,
1109- );
1110- return createFakeWindow(urlString);
1111- }
1112-
1113- // Active WebView check
1114- if (!window.__dumber_is_active) {
1115- console.log(
1116- `[window-open] [WebView ${webviewId}] BLOCKED: Inactive WebView`,
1117- );
1118- return createFakeWindow(urlString);
1119- }
1120-
1121- // User interaction check
1122- if (!isUserInteraction()) {
1123- console.log(
1124- `[window-open] [WebView ${webviewId}] BLOCKED: No recent user interaction`,
1125- );
1126- return createFakeWindow("");
1127- }
1128-
1129- // Register request and get unique ID
1130- const requestId = windowOpenDebouncer.registerRequest(
1131- urlString,
1132- webviewId,
1133- );
1134-
1135- const intent: WindowIntent = {
1136- url: urlString,
1137- target: target || "_blank",
1138- features: features || "",
1139- timestamp: Date.now(),
1140- userTriggered: true,
1141- requestId, // NEW: Add unique request ID
1142- windowType: detectWindowType(features), // Detect popup vs tab
1143- };
1144-
1145- // Send to Go backend
1146- try {
1147- if (window.webkit?.messageHandlers?.dumber) {
1148- window.webkit.messageHandlers.dumber.postMessage(
1149- JSON.stringify({
1150- type: "handle-window-open",
1151- payload: intent,
1152- }),
1153- );
1154- console.log(
1155- `[window-open] [WebView ${webviewId}] Sent request ${requestId} to Go backend`,
1156- );
1157- }
1158- } catch (err) {
1159- console.error(
1160- `[window-open] [WebView ${webviewId}] Failed to send request ${requestId}:`,
1161- err,
1162- );
1163- }
1164-
1165- return createFakeWindow(urlString, requestId);
1166- };
1167-
1168- window.__dumber_window_open_intercepted = true;
1169- console.log(
1170- `[window-open] [WebView ${window.__dumber_webview_id}] ✅ Page-world bridge installed`,
1171- );
1172- } catch (error) {
1173- console.error(
1174- `[window-open] [WebView ${window.__dumber_webview_id}] ❌ Failed to install page-world bridge:`,
1175- error,
1176- );
1177- }
1178- }
1179-
1180 const teardown = () => {861 const teardown = () => {
1181 if (teardownExecuted) {862 if (teardownExecuted) {
1182 return;863 return;
@@ -1198,7 +879,6 @@ function detectWindowType(features?: string | null): string {
1198 omniboxQueue.length = 0;879 omniboxQueue.length = 0;
1199 omniboxReady = false;880 omniboxReady = false;
1200 881
1201- window.__dumber_window_open_intercepted = false;
1202 window.__dumber_page_bridge_installed = false;882 window.__dumber_page_bridge_installed = false;
1203 window.__dumber_teardown = undefined;883 window.__dumber_teardown = undefined;
1204 window.__dumber_omnibox_suggestions = undefined;884 window.__dumber_omnibox_suggestions = undefined;

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 8

Tags 42

↑↓ moveenter openesc close