detached 1eff705

refactor(homepage): migrate from http fetch to webkit message bridge for api communication

commit
1eff7050e0d3e3d7b1049a4d5b81e7102dcc8f16
author
bamen · · 2025-09-15 16:57 UTC
committer
bamen · · 2025-09-15 19:38 UTC
parent
dbf9169
signature
unsigned
2 files · +199 -49
M gui/src/pages/Homepage.svelte +174 -47
@@ -108,20 +108,110 @@
108108 shortcutsLoading = false;
109109 };
110110
111- // Fetch recent history from the API
111+ // Setup message bridge callbacks (once)
112+ let callbacksInitialized = false;
113+ const pendingRequests = new Map();
114+
115+ const initializeCallbacks = () => {
116+ if (callbacksInitialized) return;
117+
118+ (window as any).__dumber_history_recent = (data: any[], requestId?: string) => {
119+ const request = pendingRequests.get(requestId || 'default');
120+ if (request) {
121+ request.resolve(data);
122+ pendingRequests.delete(requestId || 'default');
123+ }
124+ };
125+
126+ (window as any).__dumber_history_error = (error: string, requestId?: string) => {
127+ const request = pendingRequests.get(requestId || 'default');
128+ if (request) {
129+ request.reject(new Error(error));
130+ pendingRequests.delete(requestId || 'default');
131+ }
132+ };
133+
134+ // Omnibox suggestions callback - forward to injected script via DOM event bridge
135+ let _pendingSuggestions: any[] | null = null;
136+ (window as any).__dumber_omnibox_suggestions = (suggestions: any[]) => {
137+ try {
138+ console.log('🎯 [HOMEPAGE] Received omnibox suggestions:', suggestions?.length || 0, 'items');
139+ // Dispatch an event that injected-world listener can consume
140+ const evt = new CustomEvent('dumber:omnibox-suggestions', {
141+ detail: { suggestions }
142+ });
143+ document.dispatchEvent(evt);
144+ _pendingSuggestions = null; // clear cache on successful dispatch
145+ } catch (err) {
146+ console.warn('⚠️ [HOMEPAGE] Failed to dispatch suggestions event, caching:', err);
147+ _pendingSuggestions = suggestions;
148+ }
149+ };
150+
151+ // If omnibox gets ready later, re-send any pending suggestions
152+ document.addEventListener('dumber:omnibox-ready', () => {
153+ if (_pendingSuggestions && Array.isArray(_pendingSuggestions)) {
154+ try {
155+ const evt = new CustomEvent('dumber:omnibox-suggestions', {
156+ detail: { suggestions: _pendingSuggestions }
157+ });
158+ document.dispatchEvent(evt);
159+ _pendingSuggestions = null;
160+ console.log('🔄 [HOMEPAGE] Re-dispatched pending suggestions after omnibox-ready');
161+ } catch (e) {
162+ console.warn('⚠️ [HOMEPAGE] Failed to re-dispatch pending suggestions:', e);
163+ }
164+ }
165+ });
166+
167+ callbacksInitialized = true;
168+ };
169+
170+ const sendHistoryRequest = (type: string, params: any): Promise<any[]> => {
171+ initializeCallbacks();
172+
173+ const requestId = `${type}_${Date.now()}_${Math.random()}`;
174+
175+ return new Promise((resolve, reject) => {
176+ pendingRequests.set(requestId, { resolve, reject });
177+
178+ // Timeout after 10 seconds
179+ setTimeout(() => {
180+ if (pendingRequests.has(requestId)) {
181+ pendingRequests.delete(requestId);
182+ reject(new Error('Request timeout'));
183+ }
184+ }, 10000);
185+
186+ try {
187+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
188+ if (bridge && typeof bridge.postMessage === 'function') {
189+ bridge.postMessage(JSON.stringify({
190+ ...params,
191+ type,
192+ requestId
193+ }));
194+ } else {
195+ reject(new Error('WebKit message handler not available'));
196+ }
197+ } catch (error) {
198+ pendingRequests.delete(requestId);
199+ reject(error);
200+ }
201+ });
202+ };
203+
204+ // Fetch recent history via message bridge
112205 const fetchHistory = async () => {
113206 try {
114- const response = await fetch('dumb://homepage/api/history/recent?limit=20');
115- if (response.ok) {
116- const data = await response.json();
117- historyItems = Array.isArray(data) ? data : [];
118- historyOffset = historyItems.length;
119- hasMoreHistory = data.length === 20; // If we got full batch, there might be more
120- } else {
121- console.warn('Failed to fetch history:', response.status);
122- historyItems = [];
123- hasMoreHistory = false;
124- }
207+ const data = await sendHistoryRequest('history_recent', {
208+ limit: 20,
209+ offset: 0
210+ });
211+
212+ historyItems = Array.isArray(data) ? data : [];
213+ historyOffset = historyItems.length;
214+ hasMoreHistory = data.length === 20;
125215 } catch (error) {
126216 console.error('Error fetching history:', error);
127217 historyItems = [];
@@ -137,18 +227,16 @@
137227
138228 loadingMoreHistory = true;
139229 try {
140- const response = await fetch(`dumb://homepage/api/history/recent?limit=20&offset=${historyOffset}`);
141- if (response.ok) {
142- const data = await response.json();
143- if (Array.isArray(data) && data.length > 0) {
144- historyItems = [...historyItems, ...data];
145- historyOffset += data.length;
146- hasMoreHistory = data.length === 20; // If we got full batch, there might be more
147- } else {
148- hasMoreHistory = false;
149- }
230+ const data = await sendHistoryRequest('history_recent', {
231+ limit: 20,
232+ offset: historyOffset
233+ });
234+
235+ if (Array.isArray(data) && data.length > 0) {
236+ historyItems = [...historyItems, ...data];
237+ historyOffset += data.length;
238+ hasMoreHistory = data.length === 20; // If we got full batch, there might be more
150239 } else {
151- console.warn('Failed to fetch more history:', response.status);
152240 hasMoreHistory = false;
153241 }
154242 } catch (error) {
@@ -174,8 +262,8 @@
174262 }
175263
176264 try {
177- const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`);
178- if (response.ok) {
265+ // Set up response handlers
266+ (window as any).__dumber_history_deleted = async () => {
179267 // Wait a bit for the fade animation to complete
180268 await new Promise(resolve => setTimeout(resolve, 200));
181269
@@ -185,19 +273,32 @@
185273 await fetchHistory();
186274 await fetchStats();
187275 await fetchTopVisited();
188- } else {
189- console.error('Failed to delete history entry:', response.status);
276+
277+ // Remove from deleting array after refresh
278+ deletingIds = deletingIds.filter(i => i !== id);
279+ };
280+
281+ (window as any).__dumber_history_error = (error: string) => {
282+ console.error('Error deleting history entry:', error);
190283 // Remove from deleting array if failed
191284 deletingIds = deletingIds.filter(i => i !== id);
285+ };
286+
287+ // Send message to Go backend
288+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
289+ if (bridge && typeof bridge.postMessage === 'function') {
290+ bridge.postMessage(JSON.stringify({
291+ type: 'history_delete',
292+ historyId: id.toString()
293+ }));
294+ } else {
295+ throw new Error('WebKit message handler not available');
192296 }
193297 } catch (error) {
194- console.error('Error deleting history entry:', error);
298+ console.error('Error sending delete request:', error);
195299 // Remove from deleting array if failed
196300 deletingIds = deletingIds.filter(i => i !== id);
197301 }
198-
199- // Remove from deleting array after refresh
200- deletingIds = deletingIds.filter(i => i !== id);
201302 };
202303
203304 // Get domain from URL
@@ -231,40 +332,66 @@
231332 }
232333 };
233334
234- // Fetch history stats from the API
335+ // Fetch history stats via message bridge
235336 const fetchStats = async () => {
236337 try {
237- const response = await fetch('dumb://homepage/api/history/stats');
238- if (response.ok) {
239- const data = await response.json();
338+ // Set up response handler
339+ (window as any).__dumber_history_stats = (data: any) => {
240340 stats = data;
241- } else {
242- console.warn('Failed to fetch stats:', response.status);
341+ statsLoading = false;
342+ };
343+
344+ (window as any).__dumber_history_error = (error: string) => {
345+ console.error('Error fetching stats:', error);
243346 stats = null;
347+ statsLoading = false;
348+ };
349+
350+ // Send message to Go backend
351+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
352+ if (bridge && typeof bridge.postMessage === 'function') {
353+ bridge.postMessage(JSON.stringify({
354+ type: 'history_stats'
355+ }));
356+ } else {
357+ throw new Error('WebKit message handler not available');
244358 }
245359 } catch (error) {
246- console.error('Error fetching stats:', error);
360+ console.error('Error sending stats request:', error);
247361 stats = null;
248- } finally {
249362 statsLoading = false;
250363 }
251364 };
252365
253- // Fetch top visited sites using search endpoint sorted by visit count
366+ // Fetch top visited sites via message bridge
254367 const fetchTopVisited = async () => {
255368 try {
256- const response = await fetch('dumb://homepage/api/history/search?q=&limit=5');
257- if (response.ok) {
258- const data = await response.json();
369+ // Set up response handler
370+ (window as any).__dumber_history_search = (data: any[]) => {
259371 topVisited = Array.isArray(data) ? data.slice(0, 5) : [];
260- } else {
261- console.warn('Failed to fetch top visited:', response.status);
372+ topVisitedLoading = false;
373+ };
374+
375+ (window as any).__dumber_history_error = (error: string) => {
376+ console.error('Error fetching top visited:', error);
262377 topVisited = [];
378+ topVisitedLoading = false;
379+ };
380+
381+ // Send message to Go backend
382+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
383+ if (bridge && typeof bridge.postMessage === 'function') {
384+ bridge.postMessage(JSON.stringify({
385+ type: 'history_search',
386+ q: '',
387+ limit: 5
388+ }));
389+ } else {
390+ throw new Error('WebKit message handler not available');
263391 }
264392 } catch (error) {
265- console.error('Error fetching top visited:', error);
393+ console.error('Error sending top visited request:', error);
266394 topVisited = [];
267- } finally {
268395 topVisitedLoading = false;
269396 }
270397 };
@@ -108,20 +108,110 @@
108 shortcutsLoading = false;108 shortcutsLoading = false;
109 };109 };
110 110
111- // Fetch recent history from the API111+ // Setup message bridge callbacks (once)
112+ let callbacksInitialized = false;
113+ const pendingRequests = new Map();
114+
115+ const initializeCallbacks = () => {
116+ if (callbacksInitialized) return;
117+
118+ (window as any).__dumber_history_recent = (data: any[], requestId?: string) => {
119+ const request = pendingRequests.get(requestId || 'default');
120+ if (request) {
121+ request.resolve(data);
122+ pendingRequests.delete(requestId || 'default');
123+ }
124+ };
125+
126+ (window as any).__dumber_history_error = (error: string, requestId?: string) => {
127+ const request = pendingRequests.get(requestId || 'default');
128+ if (request) {
129+ request.reject(new Error(error));
130+ pendingRequests.delete(requestId || 'default');
131+ }
132+ };
133+
134+ // Omnibox suggestions callback - forward to injected script via DOM event bridge
135+ let _pendingSuggestions: any[] | null = null;
136+ (window as any).__dumber_omnibox_suggestions = (suggestions: any[]) => {
137+ try {
138+ console.log('🎯 [HOMEPAGE] Received omnibox suggestions:', suggestions?.length || 0, 'items');
139+ // Dispatch an event that injected-world listener can consume
140+ const evt = new CustomEvent('dumber:omnibox-suggestions', {
141+ detail: { suggestions }
142+ });
143+ document.dispatchEvent(evt);
144+ _pendingSuggestions = null; // clear cache on successful dispatch
145+ } catch (err) {
146+ console.warn('⚠️ [HOMEPAGE] Failed to dispatch suggestions event, caching:', err);
147+ _pendingSuggestions = suggestions;
148+ }
149+ };
150+
151+ // If omnibox gets ready later, re-send any pending suggestions
152+ document.addEventListener('dumber:omnibox-ready', () => {
153+ if (_pendingSuggestions && Array.isArray(_pendingSuggestions)) {
154+ try {
155+ const evt = new CustomEvent('dumber:omnibox-suggestions', {
156+ detail: { suggestions: _pendingSuggestions }
157+ });
158+ document.dispatchEvent(evt);
159+ _pendingSuggestions = null;
160+ console.log('🔄 [HOMEPAGE] Re-dispatched pending suggestions after omnibox-ready');
161+ } catch (e) {
162+ console.warn('⚠️ [HOMEPAGE] Failed to re-dispatch pending suggestions:', e);
163+ }
164+ }
165+ });
166+
167+ callbacksInitialized = true;
168+ };
169+
170+ const sendHistoryRequest = (type: string, params: any): Promise<any[]> => {
171+ initializeCallbacks();
172+
173+ const requestId = `${type}_${Date.now()}_${Math.random()}`;
174+
175+ return new Promise((resolve, reject) => {
176+ pendingRequests.set(requestId, { resolve, reject });
177+
178+ // Timeout after 10 seconds
179+ setTimeout(() => {
180+ if (pendingRequests.has(requestId)) {
181+ pendingRequests.delete(requestId);
182+ reject(new Error('Request timeout'));
183+ }
184+ }, 10000);
185+
186+ try {
187+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
188+ if (bridge && typeof bridge.postMessage === 'function') {
189+ bridge.postMessage(JSON.stringify({
190+ ...params,
191+ type,
192+ requestId
193+ }));
194+ } else {
195+ reject(new Error('WebKit message handler not available'));
196+ }
197+ } catch (error) {
198+ pendingRequests.delete(requestId);
199+ reject(error);
200+ }
201+ });
202+ };
203+
204+ // Fetch recent history via message bridge
112 const fetchHistory = async () => {205 const fetchHistory = async () => {
113 try {206 try {
114- const response = await fetch('dumb://homepage/api/history/recent?limit=20');207+ const data = await sendHistoryRequest('history_recent', {
115- if (response.ok) {208+ limit: 20,
116- const data = await response.json();209+ offset: 0
117- historyItems = Array.isArray(data) ? data : [];210+ });
118- historyOffset = historyItems.length;211+
119- hasMoreHistory = data.length === 20; // If we got full batch, there might be more212+ historyItems = Array.isArray(data) ? data : [];
120- } else {213+ historyOffset = historyItems.length;
121- console.warn('Failed to fetch history:', response.status);214+ hasMoreHistory = data.length === 20;
122- historyItems = [];
123- hasMoreHistory = false;
124- }
125 } catch (error) {215 } catch (error) {
126 console.error('Error fetching history:', error);216 console.error('Error fetching history:', error);
127 historyItems = [];217 historyItems = [];
@@ -137,18 +227,16 @@
137 227
138 loadingMoreHistory = true;228 loadingMoreHistory = true;
139 try {229 try {
140- const response = await fetch(`dumb://homepage/api/history/recent?limit=20&offset=${historyOffset}`);230+ const data = await sendHistoryRequest('history_recent', {
141- if (response.ok) {231+ limit: 20,
142- const data = await response.json();232+ offset: historyOffset
143- if (Array.isArray(data) && data.length > 0) {233+ });
144- historyItems = [...historyItems, ...data];234+
145- historyOffset += data.length;235+ if (Array.isArray(data) && data.length > 0) {
146- hasMoreHistory = data.length === 20; // If we got full batch, there might be more236+ historyItems = [...historyItems, ...data];
147- } else {237+ historyOffset += data.length;
148- hasMoreHistory = false;238+ hasMoreHistory = data.length === 20; // If we got full batch, there might be more
149- }
150 } else {239 } else {
151- console.warn('Failed to fetch more history:', response.status);
152 hasMoreHistory = false;240 hasMoreHistory = false;
153 }241 }
154 } catch (error) {242 } catch (error) {
@@ -174,8 +262,8 @@
174 }262 }
175 263
176 try {264 try {
177- const response = await fetch(`dumb://homepage/api/history/delete?id=${id}`);265+ // Set up response handlers
178- if (response.ok) {266+ (window as any).__dumber_history_deleted = async () => {
179 // Wait a bit for the fade animation to complete267 // Wait a bit for the fade animation to complete
180 await new Promise(resolve => setTimeout(resolve, 200));268 await new Promise(resolve => setTimeout(resolve, 200));
181 269
@@ -185,19 +273,32 @@
185 await fetchHistory();273 await fetchHistory();
186 await fetchStats();274 await fetchStats();
187 await fetchTopVisited();275 await fetchTopVisited();
188- } else {276+
189- console.error('Failed to delete history entry:', response.status);277+ // Remove from deleting array after refresh
278+ deletingIds = deletingIds.filter(i => i !== id);
279+ };
280+
281+ (window as any).__dumber_history_error = (error: string) => {
282+ console.error('Error deleting history entry:', error);
190 // Remove from deleting array if failed283 // Remove from deleting array if failed
191 deletingIds = deletingIds.filter(i => i !== id);284 deletingIds = deletingIds.filter(i => i !== id);
285+ };
286+
287+ // Send message to Go backend
288+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
289+ if (bridge && typeof bridge.postMessage === 'function') {
290+ bridge.postMessage(JSON.stringify({
291+ type: 'history_delete',
292+ historyId: id.toString()
293+ }));
294+ } else {
295+ throw new Error('WebKit message handler not available');
192 }296 }
193 } catch (error) {297 } catch (error) {
194- console.error('Error deleting history entry:', error);298+ console.error('Error sending delete request:', error);
195 // Remove from deleting array if failed299 // Remove from deleting array if failed
196 deletingIds = deletingIds.filter(i => i !== id);300 deletingIds = deletingIds.filter(i => i !== id);
197 }301 }
198-
199- // Remove from deleting array after refresh
200- deletingIds = deletingIds.filter(i => i !== id);
201 };302 };
202 303
203 // Get domain from URL304 // Get domain from URL
@@ -231,40 +332,66 @@
231 }332 }
232 };333 };
233 334
234- // Fetch history stats from the API335+ // Fetch history stats via message bridge
235 const fetchStats = async () => {336 const fetchStats = async () => {
236 try {337 try {
237- const response = await fetch('dumb://homepage/api/history/stats');338+ // Set up response handler
238- if (response.ok) {339+ (window as any).__dumber_history_stats = (data: any) => {
239- const data = await response.json();
240 stats = data;340 stats = data;
241- } else {341+ statsLoading = false;
242- console.warn('Failed to fetch stats:', response.status);342+ };
343+
344+ (window as any).__dumber_history_error = (error: string) => {
345+ console.error('Error fetching stats:', error);
243 stats = null;346 stats = null;
347+ statsLoading = false;
348+ };
349+
350+ // Send message to Go backend
351+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
352+ if (bridge && typeof bridge.postMessage === 'function') {
353+ bridge.postMessage(JSON.stringify({
354+ type: 'history_stats'
355+ }));
356+ } else {
357+ throw new Error('WebKit message handler not available');
244 }358 }
245 } catch (error) {359 } catch (error) {
246- console.error('Error fetching stats:', error);360+ console.error('Error sending stats request:', error);
247 stats = null;361 stats = null;
248- } finally {
249 statsLoading = false;362 statsLoading = false;
250 }363 }
251 };364 };
252 365
253- // Fetch top visited sites using search endpoint sorted by visit count366+ // Fetch top visited sites via message bridge
254 const fetchTopVisited = async () => {367 const fetchTopVisited = async () => {
255 try {368 try {
256- const response = await fetch('dumb://homepage/api/history/search?q=&limit=5');369+ // Set up response handler
257- if (response.ok) {370+ (window as any).__dumber_history_search = (data: any[]) => {
258- const data = await response.json();
259 topVisited = Array.isArray(data) ? data.slice(0, 5) : [];371 topVisited = Array.isArray(data) ? data.slice(0, 5) : [];
260- } else {372+ topVisitedLoading = false;
261- console.warn('Failed to fetch top visited:', response.status);373+ };
374+
375+ (window as any).__dumber_history_error = (error: string) => {
376+ console.error('Error fetching top visited:', error);
262 topVisited = [];377 topVisited = [];
378+ topVisitedLoading = false;
379+ };
380+
381+ // Send message to Go backend
382+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
383+ if (bridge && typeof bridge.postMessage === 'function') {
384+ bridge.postMessage(JSON.stringify({
385+ type: 'history_search',
386+ q: '',
387+ limit: 5
388+ }));
389+ } else {
390+ throw new Error('WebKit message handler not available');
263 }391 }
264 } catch (error) {392 } catch (error) {
265- console.error('Error fetching top visited:', error);393+ console.error('Error sending top visited request:', error);
266 topVisited = [];394 topVisited = [];
267- } finally {
268 topVisitedLoading = false;395 topVisitedLoading = false;
269 }396 }
270 };397 };
M gui/src/services/app.ts +25 -2
@@ -26,8 +26,31 @@ export class AppService {
2626
2727 async loadHistory(): Promise<void> {
2828 try {
29- const recentHistory: HistoryEntry[] = await fetch('/api/history/recent?limit=50').then(r => r.json());
30- this.history = recentHistory;
29+ // Use message bridge instead of fetch
30+ return new Promise((resolve, reject) => {
31+ // Set up response handler
32+ (window as any).__dumber_history_recent = (data: HistoryEntry[]) => {
33+ this.history = Array.isArray(data) ? data : [];
34+ resolve();
35+ };
36+
37+ (window as any).__dumber_history_error = (error: string) => {
38+ console.error('Failed to load history:', error);
39+ reject(new Error(error));
40+ };
41+
42+ // Send message to Go backend
43+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
44+ if (bridge && typeof bridge.postMessage === 'function') {
45+ bridge.postMessage(JSON.stringify({
46+ type: 'history_recent',
47+ limit: 50,
48+ offset: 0
49+ }));
50+ } else {
51+ reject(new Error('WebKit message handler not available'));
52+ }
53+ });
3154 } catch (error) {
3255 console.error('Failed to load history:', error);
3356 // Mock history for development/fallback
@@ -26,8 +26,31 @@ export class AppService {
26 26
27 async loadHistory(): Promise<void> {27 async loadHistory(): Promise<void> {
28 try {28 try {
29- const recentHistory: HistoryEntry[] = await fetch('/api/history/recent?limit=50').then(r => r.json());29+ // Use message bridge instead of fetch
30- this.history = recentHistory;30+ return new Promise((resolve, reject) => {
31+ // Set up response handler
32+ (window as any).__dumber_history_recent = (data: HistoryEntry[]) => {
33+ this.history = Array.isArray(data) ? data : [];
34+ resolve();
35+ };
36+
37+ (window as any).__dumber_history_error = (error: string) => {
38+ console.error('Failed to load history:', error);
39+ reject(new Error(error));
40+ };
41+
42+ // Send message to Go backend
43+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
44+ if (bridge && typeof bridge.postMessage === 'function') {
45+ bridge.postMessage(JSON.stringify({
46+ type: 'history_recent',
47+ limit: 50,
48+ offset: 0
49+ }));
50+ } else {
51+ reject(new Error('WebKit message handler not available'));
52+ }
53+ });
31 } catch (error) {54 } catch (error) {
32 console.error('Failed to load history:', error);55 console.error('Failed to load history:', error);
33 // Mock history for development/fallback56 // Mock history for development/fallback

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