detached fa675ef

refactor(wiring): wire message handlers into UI layer and update frontend messaging

commit
fa675efe9479770eff9d1eb2a891e6c75b0d6cd3
author
brice · · 2025-12-16 17:22 UTC
committer
brice · · 2025-12-16 21:27 UTC
parent
f42505b
signature
unsigned
3 files · +93 -36
M .mockery.yaml +13 -0
@@ -1,4 +1,17 @@
11 packages:
2+ github.com/bnema/dumber/internal/domain/repository:
3+ config:
4+ dir: "{{.InterfaceDir}}/mocks"
5+ filename: "mock_{{.InterfaceName | snakecase}}.go"
6+ structname: "Mock{{.InterfaceName}}"
7+ pkgname: "mocks"
8+ interfaces:
9+ HistoryRepository:
10+ FavoriteRepository:
11+ FolderRepository:
12+ TagRepository:
13+ ZoomRepository:
14+ ContentWhitelistRepository:
215 github.com/bnema/dumber/internal/application/port:
316 config:
417 dir: "{{.InterfaceDir}}/mocks"
@@ -1,4 +1,17 @@
1 packages:1 packages:
2+ github.com/bnema/dumber/internal/domain/repository:
3+ config:
4+ dir: "{{.InterfaceDir}}/mocks"
5+ filename: "mock_{{.InterfaceName | snakecase}}.go"
6+ structname: "Mock{{.InterfaceName}}"
7+ pkgname: "mocks"
8+ interfaces:
9+ HistoryRepository:
10+ FavoriteRepository:
11+ FolderRepository:
12+ TagRepository:
13+ ZoomRepository:
14+ ContentWhitelistRepository:
2 github.com/bnema/dumber/internal/application/port:15 github.com/bnema/dumber/internal/application/port:
3 config:16 config:
4 dir: "{{.InterfaceDir}}/mocks"17 dir: "{{.InterfaceDir}}/mocks"
M internal/ui/app.go +11 -0
@@ -12,6 +12,7 @@ import (
1212 "github.com/bnema/dumber/internal/application/usecase"
1313 "github.com/bnema/dumber/internal/domain/entity"
1414 "github.com/bnema/dumber/internal/infrastructure/webkit"
15+ "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"
1516 "github.com/bnema/dumber/internal/logging"
1617 "github.com/bnema/dumber/internal/ui/cache"
1718 "github.com/bnema/dumber/internal/ui/component"
@@ -100,6 +101,16 @@ func New(deps *Dependencies) (*App, error) {
100101 app.router = webkit.NewMessageRouter(ctx)
101102 }
102103
104+ // Register message handlers
105+ if app.router != nil && deps.HistoryUC != nil && deps.FavoritesUC != nil {
106+ if err := handlers.RegisterAll(ctx, app.router, handlers.Config{
107+ HistoryUC: deps.HistoryUC,
108+ FavoritesUC: deps.FavoritesUC,
109+ }); err != nil {
110+ return nil, err
111+ }
112+ }
113+
103114 return app, nil
104115 }
105116
@@ -12,6 +12,7 @@ import (
12 "github.com/bnema/dumber/internal/application/usecase"12 "github.com/bnema/dumber/internal/application/usecase"
13 "github.com/bnema/dumber/internal/domain/entity"13 "github.com/bnema/dumber/internal/domain/entity"
14 "github.com/bnema/dumber/internal/infrastructure/webkit"14 "github.com/bnema/dumber/internal/infrastructure/webkit"
15+ "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"
15 "github.com/bnema/dumber/internal/logging"16 "github.com/bnema/dumber/internal/logging"
16 "github.com/bnema/dumber/internal/ui/cache"17 "github.com/bnema/dumber/internal/ui/cache"
17 "github.com/bnema/dumber/internal/ui/component"18 "github.com/bnema/dumber/internal/ui/component"
@@ -100,6 +101,16 @@ func New(deps *Dependencies) (*App, error) {
100 app.router = webkit.NewMessageRouter(ctx)101 app.router = webkit.NewMessageRouter(ctx)
101 }102 }
102 103
104+ // Register message handlers
105+ if app.router != nil && deps.HistoryUC != nil && deps.FavoritesUC != nil {
106+ if err := handlers.RegisterAll(ctx, app.router, handlers.Config{
107+ HistoryUC: deps.HistoryUC,
108+ FavoritesUC: deps.FavoritesUC,
109+ }); err != nil {
110+ return nil, err
111+ }
112+ }
113+
103 return app, nil114 return app, nil
104 }115 }
105 116
M webui/src/pages/homepage/messaging.ts +69 -36
@@ -46,6 +46,10 @@ function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
4646 return null;
4747 }
4848
49+function getWebViewId(): number {
50+ return (window as any).__dumber_webview_id || 0;
51+}
52+
4953 function generateRequestId(type: string): string {
5054 return `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
5155 }
@@ -57,69 +61,75 @@ function generateRequestId(type: string): string {
5761 function initializeCallbacks(): void {
5862 if (callbacksInitialized) return;
5963
60- // Generic response handler
64+ // Generic response handler - accepts a single Response object from Go backend
6165 (window as any).__dumber_homepage_response = (
62- requestId: string,
63- success: boolean,
64- data: unknown,
65- error?: string
66+ response: { requestId: string; success: boolean; data?: unknown; error?: string }
6667 ) => {
67- const request = pendingRequests.get(requestId);
68+ const request = pendingRequests.get(response.requestId);
6869 if (!request) return;
6970
7071 clearTimeout(request.timeout);
71- pendingRequests.delete(requestId);
72+ pendingRequests.delete(response.requestId);
7273
73- if (success) {
74- request.resolve(data);
74+ if (response.success) {
75+ request.resolve(response.data);
7576 } else {
76- request.reject(new Error(error || 'Unknown error'));
77+ request.reject(new Error(response.error || 'Unknown error'));
7778 }
7879 };
7980
8081 // Legacy callbacks for backward compatibility
81- (window as any).__dumber_history_timeline = (data: unknown[], requestId?: string) => {
82- handleLegacyCallback(requestId, data);
82+ // These can receive either raw data or a Response object
83+ (window as any).__dumber_history_timeline = (dataOrResponse: unknown) => {
84+ handleLegacyOrResponseCallback(dataOrResponse);
8385 };
8486
85- (window as any).__dumber_folders = (data: unknown[], requestId?: string) => {
86- handleLegacyCallback(requestId, data);
87+ (window as any).__dumber_folders = (dataOrResponse: unknown) => {
88+ handleLegacyOrResponseCallback(dataOrResponse);
8789 };
8890
89- (window as any).__dumber_tags = (data: unknown[], requestId?: string) => {
90- handleLegacyCallback(requestId, data);
91+ (window as any).__dumber_tags = (dataOrResponse: unknown) => {
92+ handleLegacyOrResponseCallback(dataOrResponse);
9193 };
9294
93- (window as any).__dumber_favorites = (data: unknown[], requestId?: string) => {
94- handleLegacyCallback(requestId, data);
95+ (window as any).__dumber_favorites = (dataOrResponse: unknown) => {
96+ handleLegacyOrResponseCallback(dataOrResponse);
9597 };
9698
97- (window as any).__dumber_analytics = (data: unknown, requestId?: string) => {
98- handleLegacyCallback(requestId, data);
99+ (window as any).__dumber_analytics = (dataOrResponse: unknown) => {
100+ handleLegacyOrResponseCallback(dataOrResponse);
99101 };
100102
101- (window as any).__dumber_domain_stats = (data: unknown[], requestId?: string) => {
102- handleLegacyCallback(requestId, data);
103+ (window as any).__dumber_domain_stats = (dataOrResponse: unknown) => {
104+ handleLegacyOrResponseCallback(dataOrResponse);
103105 };
104106
105- (window as any).__dumber_history_search_results = (data: unknown[], requestId?: string) => {
106- handleLegacyCallback(requestId, data);
107+ (window as any).__dumber_history_search_results = (dataOrResponse: unknown) => {
108+ handleLegacyOrResponseCallback(dataOrResponse);
107109 };
108110
109- (window as any).__dumber_history_deleted = (requestId?: string) => {
110- handleLegacyCallback(requestId, { success: true });
111+ (window as any).__dumber_history_deleted = (dataOrResponse: unknown) => {
112+ handleLegacyOrResponseCallback(dataOrResponse);
111113 };
112114
113- (window as any).__dumber_history_cleared = (requestId?: string) => {
114- handleLegacyCallback(requestId, { success: true });
115+ (window as any).__dumber_history_cleared = (dataOrResponse: unknown) => {
116+ handleLegacyOrResponseCallback(dataOrResponse);
115117 };
116118
117- (window as any).__dumber_error = (error: string, requestId?: string) => {
118- const request = pendingRequests.get(requestId || 'default');
119- if (request) {
120- clearTimeout(request.timeout);
121- pendingRequests.delete(requestId || 'default');
122- request.reject(new Error(error));
119+ (window as any).__dumber_error = (errorOrResponse: unknown) => {
120+ // Handle both formats: direct error string or Response object with error
121+ const response = errorOrResponse as { requestId?: string; error?: string } | string;
122+ if (typeof response === 'string') {
123+ // Legacy format: direct error string
124+ console.error('[messaging] Legacy error:', response);
125+ } else if (response && typeof response === 'object') {
126+ // Response object format
127+ const request = pendingRequests.get(response.requestId || 'default');
128+ if (request) {
129+ clearTimeout(request.timeout);
130+ pendingRequests.delete(response.requestId || 'default');
131+ request.reject(new Error(response.error || 'Unknown error'));
132+ }
123133 }
124134 };
125135
@@ -141,6 +151,29 @@ function handleLegacyCallback(requestId: string | undefined, data: unknown): voi
141151 }
142152 }
143153
154+// Handle both legacy raw data and new Response object format
155+function handleLegacyOrResponseCallback(dataOrResponse: unknown): void {
156+ // Check if it's a Response object (has requestId and success fields)
157+ const response = dataOrResponse as { requestId?: string; success?: boolean; data?: unknown; error?: string };
158+ if (response && typeof response === 'object' && 'requestId' in response && 'success' in response) {
159+ // New Response object format
160+ const request = pendingRequests.get(response.requestId || 'default');
161+ if (request) {
162+ clearTimeout(request.timeout);
163+ pendingRequests.delete(response.requestId || 'default');
164+ if (response.success) {
165+ request.resolve(response.data);
166+ } else {
167+ request.reject(new Error(response.error || 'Unknown error'));
168+ }
169+ }
170+ } else {
171+ // Legacy format: raw data (for backward compatibility)
172+ // Try to find any pending request (not ideal but maintains compatibility)
173+ console.warn('[messaging] Received legacy callback format without requestId');
174+ }
175+}
176+
144177 // ═══════════════════════════════════════════════════════════════════════════════
145178 // CORE SEND FUNCTION
146179 // ═══════════════════════════════════════════════════════════════════════════════
@@ -169,8 +202,8 @@ async function sendMessage<T = unknown>(
169202 try {
170203 bridge.postMessage({
171204 type,
172- requestId,
173- ...payload,
205+ webviewId: getWebViewId(),
206+ payload: { requestId, ...payload },
174207 });
175208 } catch (error) {
176209 clearTimeout(timeout);
@@ -46,6 +46,10 @@ function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
46 return null;46 return null;
47 }47 }
48 48
49+function getWebViewId(): number {
50+ return (window as any).__dumber_webview_id || 0;
51+}
52+
49 function generateRequestId(type: string): string {53 function generateRequestId(type: string): string {
50 return `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;54 return `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
51 }55 }
@@ -57,69 +61,75 @@ function generateRequestId(type: string): string {
57 function initializeCallbacks(): void {61 function initializeCallbacks(): void {
58 if (callbacksInitialized) return;62 if (callbacksInitialized) return;
59 63
60- // Generic response handler64+ // Generic response handler - accepts a single Response object from Go backend
61 (window as any).__dumber_homepage_response = (65 (window as any).__dumber_homepage_response = (
62- requestId: string,66+ response: { requestId: string; success: boolean; data?: unknown; error?: string }
63- success: boolean,
64- data: unknown,
65- error?: string
66 ) => {67 ) => {
67- const request = pendingRequests.get(requestId);68+ const request = pendingRequests.get(response.requestId);
68 if (!request) return;69 if (!request) return;
69 70
70 clearTimeout(request.timeout);71 clearTimeout(request.timeout);
71- pendingRequests.delete(requestId);72+ pendingRequests.delete(response.requestId);
72 73
73- if (success) {74+ if (response.success) {
74- request.resolve(data);75+ request.resolve(response.data);
75 } else {76 } else {
76- request.reject(new Error(error || 'Unknown error'));77+ request.reject(new Error(response.error || 'Unknown error'));
77 }78 }
78 };79 };
79 80
80 // Legacy callbacks for backward compatibility81 // Legacy callbacks for backward compatibility
81- (window as any).__dumber_history_timeline = (data: unknown[], requestId?: string) => {82+ // These can receive either raw data or a Response object
82- handleLegacyCallback(requestId, data);83+ (window as any).__dumber_history_timeline = (dataOrResponse: unknown) => {
84+ handleLegacyOrResponseCallback(dataOrResponse);
83 };85 };
84 86
85- (window as any).__dumber_folders = (data: unknown[], requestId?: string) => {87+ (window as any).__dumber_folders = (dataOrResponse: unknown) => {
86- handleLegacyCallback(requestId, data);88+ handleLegacyOrResponseCallback(dataOrResponse);
87 };89 };
88 90
89- (window as any).__dumber_tags = (data: unknown[], requestId?: string) => {91+ (window as any).__dumber_tags = (dataOrResponse: unknown) => {
90- handleLegacyCallback(requestId, data);92+ handleLegacyOrResponseCallback(dataOrResponse);
91 };93 };
92 94
93- (window as any).__dumber_favorites = (data: unknown[], requestId?: string) => {95+ (window as any).__dumber_favorites = (dataOrResponse: unknown) => {
94- handleLegacyCallback(requestId, data);96+ handleLegacyOrResponseCallback(dataOrResponse);
95 };97 };
96 98
97- (window as any).__dumber_analytics = (data: unknown, requestId?: string) => {99+ (window as any).__dumber_analytics = (dataOrResponse: unknown) => {
98- handleLegacyCallback(requestId, data);100+ handleLegacyOrResponseCallback(dataOrResponse);
99 };101 };
100 102
101- (window as any).__dumber_domain_stats = (data: unknown[], requestId?: string) => {103+ (window as any).__dumber_domain_stats = (dataOrResponse: unknown) => {
102- handleLegacyCallback(requestId, data);104+ handleLegacyOrResponseCallback(dataOrResponse);
103 };105 };
104 106
105- (window as any).__dumber_history_search_results = (data: unknown[], requestId?: string) => {107+ (window as any).__dumber_history_search_results = (dataOrResponse: unknown) => {
106- handleLegacyCallback(requestId, data);108+ handleLegacyOrResponseCallback(dataOrResponse);
107 };109 };
108 110
109- (window as any).__dumber_history_deleted = (requestId?: string) => {111+ (window as any).__dumber_history_deleted = (dataOrResponse: unknown) => {
110- handleLegacyCallback(requestId, { success: true });112+ handleLegacyOrResponseCallback(dataOrResponse);
111 };113 };
112 114
113- (window as any).__dumber_history_cleared = (requestId?: string) => {115+ (window as any).__dumber_history_cleared = (dataOrResponse: unknown) => {
114- handleLegacyCallback(requestId, { success: true });116+ handleLegacyOrResponseCallback(dataOrResponse);
115 };117 };
116 118
117- (window as any).__dumber_error = (error: string, requestId?: string) => {119+ (window as any).__dumber_error = (errorOrResponse: unknown) => {
118- const request = pendingRequests.get(requestId || 'default');120+ // Handle both formats: direct error string or Response object with error
119- if (request) {121+ const response = errorOrResponse as { requestId?: string; error?: string } | string;
120- clearTimeout(request.timeout);122+ if (typeof response === 'string') {
121- pendingRequests.delete(requestId || 'default');123+ // Legacy format: direct error string
122- request.reject(new Error(error));124+ console.error('[messaging] Legacy error:', response);
125+ } else if (response && typeof response === 'object') {
126+ // Response object format
127+ const request = pendingRequests.get(response.requestId || 'default');
128+ if (request) {
129+ clearTimeout(request.timeout);
130+ pendingRequests.delete(response.requestId || 'default');
131+ request.reject(new Error(response.error || 'Unknown error'));
132+ }
123 }133 }
124 };134 };
125 135
@@ -141,6 +151,29 @@ function handleLegacyCallback(requestId: string | undefined, data: unknown): voi
141 }151 }
142 }152 }
143 153
154+// Handle both legacy raw data and new Response object format
155+function handleLegacyOrResponseCallback(dataOrResponse: unknown): void {
156+ // Check if it's a Response object (has requestId and success fields)
157+ const response = dataOrResponse as { requestId?: string; success?: boolean; data?: unknown; error?: string };
158+ if (response && typeof response === 'object' && 'requestId' in response && 'success' in response) {
159+ // New Response object format
160+ const request = pendingRequests.get(response.requestId || 'default');
161+ if (request) {
162+ clearTimeout(request.timeout);
163+ pendingRequests.delete(response.requestId || 'default');
164+ if (response.success) {
165+ request.resolve(response.data);
166+ } else {
167+ request.reject(new Error(response.error || 'Unknown error'));
168+ }
169+ }
170+ } else {
171+ // Legacy format: raw data (for backward compatibility)
172+ // Try to find any pending request (not ideal but maintains compatibility)
173+ console.warn('[messaging] Received legacy callback format without requestId');
174+ }
175+}
176+
144 // ═══════════════════════════════════════════════════════════════════════════════177 // ═══════════════════════════════════════════════════════════════════════════════
145 // CORE SEND FUNCTION178 // CORE SEND FUNCTION
146 // ═══════════════════════════════════════════════════════════════════════════════179 // ═══════════════════════════════════════════════════════════════════════════════
@@ -169,8 +202,8 @@ async function sendMessage<T = unknown>(
169 try {202 try {
170 bridge.postMessage({203 bridge.postMessage({
171 type,204 type,
172- requestId,205+ webviewId: getWebViewId(),
173- ...payload,206+ payload: { requestId, ...payload },
174 });207 });
175 } catch (error) {208 } catch (error) {
176 clearTimeout(timeout);209 clearTimeout(timeout);

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