detached 61536e8

fix(messaging): normalize numeric webviewId values in JSON payloads

The message handler now gracefully handles both string and numeric webviewId
formats from the frontend. When standard JSON unmarshaling fails, the parser
attempts to normalize numeric IDs to strings before re-parsing.

This prevents unmarshaling errors when frontend code sends webviewId as a
number instead of the expected string format.

Changes:
- Add parseIncomingMessage() with fallback normalization logic
- Add normalizeWebViewIDPayload() to convert numeric IDs to strings
- Add parseWebViewIDRaw() to handle both string and number formats
- Add comprehensive test coverage for both string and numeric cases

Fixes frontend-backend compatibility issues with webviewId field types.
commit
61536e8cd1ef8f17f750e9ee4c8eb42a7cf27145
author
bnema · · 2025-11-17 21:03 UTC
committer
bnema · · 2025-11-17 19:07 UTC
parent
de91544
signature
unsigned
3 files · +105 -2
M CHANGELOG.md +12 -0
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
44
55 ## [Unreleased]
66
7+### Added
8+- **Cloudflare Turnstile CAPTCHA support**: Temporary CORS allowlist workaround for `challenges.cloudflare.com` to bypass WebKit's incomplete COEP credentialless implementation. Allows Turnstile-protected sites to function properly. Configurable via `enable_turnstile_workaround` (default: true).
9+- **Global content filtering toggle**: New `content_filtering.enabled` config option to completely disable ad blocking when needed. Whitelist system remains available for granular control. Defaults to enabled for backward compatibility.
10+
11+### Fixed
12+- **OAuth popup handling**: Fixed OAuth popups not functioning in stacked panes and split operations by ensuring `setupPopupHandling()` is called in all pane creation code paths (finalizeStackCreation, CloseStackedPane, splitNode).
13+- **WebView ID message parsing**: Fixed JSON unmarshaling errors when frontend sends numeric webviewId values instead of strings. Message handler now gracefully normalizes both string and numeric webviewId formats with fallback parsing logic.
14+
15+### Changed
16+- **Console output cleanup**: Disabled WebKit console.log() messages to stdout to prevent page script spam. DevTools console (F12) still shows all messages.
17+- **Content filtering configuration**: Refactored `content_filtering_whitelist` into structured `content_filtering` config with `enabled` boolean and `whitelist` array for better organization.
18+
719 ## [0.14.0] - 2025-11-18
820
921 ### Added
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4 4
5 ## [Unreleased]5 ## [Unreleased]
6 6
7+### Added
8+- **Cloudflare Turnstile CAPTCHA support**: Temporary CORS allowlist workaround for `challenges.cloudflare.com` to bypass WebKit's incomplete COEP credentialless implementation. Allows Turnstile-protected sites to function properly. Configurable via `enable_turnstile_workaround` (default: true).
9+- **Global content filtering toggle**: New `content_filtering.enabled` config option to completely disable ad blocking when needed. Whitelist system remains available for granular control. Defaults to enabled for backward compatibility.
10+
11+### Fixed
12+- **OAuth popup handling**: Fixed OAuth popups not functioning in stacked panes and split operations by ensuring `setupPopupHandling()` is called in all pane creation code paths (finalizeStackCreation, CloseStackedPane, splitNode).
13+- **WebView ID message parsing**: Fixed JSON unmarshaling errors when frontend sends numeric webviewId values instead of strings. Message handler now gracefully normalizes both string and numeric webviewId formats with fallback parsing logic.
14+
15+### Changed
16+- **Console output cleanup**: Disabled WebKit console.log() messages to stdout to prevent page script spam. DevTools console (F12) still shows all messages.
17+- **Content filtering configuration**: Refactored `content_filtering_whitelist` into structured `content_filtering` config with `enabled` boolean and `whitelist` array for better organization.
18+
7 ## [0.14.0] - 2025-11-1819 ## [0.14.0] - 2025-11-18
8 20
9 ### Added21 ### Added
M internal/app/messaging/handler.go +66 -2
@@ -1,8 +1,10 @@
11 package messaging
22
33 import (
4+ "bytes"
45 "context"
56 "encoding/json"
7+ "fmt"
68 "log"
79 "strconv"
810 "time"
@@ -75,8 +77,8 @@ func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
7577
7678 // Handle processes incoming script messages
7779 func (h *Handler) Handle(payload string) {
78- var msg Message
79- if err := json.Unmarshal([]byte(payload), &msg); err != nil {
80+ msg, err := parseIncomingMessage(payload)
81+ if err != nil {
8082 log.Printf("[ERROR] Failed to unmarshal message: %v", err)
8183 return
8284 }
@@ -122,6 +124,68 @@ func (h *Handler) Handle(payload string) {
122124 }
123125 }
124126
127+func parseIncomingMessage(payload string) (Message, error) {
128+ data := []byte(payload)
129+ var msg Message
130+ if err := json.Unmarshal(data, &msg); err == nil {
131+ return msg, nil
132+ } else {
133+ normalized, normErr := normalizeWebViewIDPayload(data)
134+ if normErr != nil {
135+ return Message{}, err
136+ }
137+
138+ if err := json.Unmarshal(normalized, &msg); err != nil {
139+ return Message{}, err
140+ }
141+
142+ return msg, nil
143+ }
144+}
145+
146+func normalizeWebViewIDPayload(data []byte) ([]byte, error) {
147+ var raw map[string]json.RawMessage
148+ if err := json.Unmarshal(data, &raw); err != nil {
149+ return nil, err
150+ }
151+
152+ rawID, ok := raw["webviewId"]
153+ if !ok {
154+ return nil, fmt.Errorf("webviewId missing in payload")
155+ }
156+
157+ normalizedID, err := parseWebViewIDRaw(rawID)
158+ if err != nil {
159+ return nil, err
160+ }
161+
162+ raw["webviewId"] = json.RawMessage(strconv.Quote(normalizedID))
163+
164+ return json.Marshal(raw)
165+}
166+
167+func parseWebViewIDRaw(raw json.RawMessage) (string, error) {
168+ trimmed := bytes.TrimSpace(raw)
169+ if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
170+ return "", nil
171+ }
172+
173+ if trimmed[0] == '"' {
174+ var id string
175+ if err := json.Unmarshal(trimmed, &id); err != nil {
176+ return "", err
177+ }
178+ return id, nil
179+ }
180+
181+ var number json.Number
182+ if err := json.Unmarshal(trimmed, &number); err == nil {
183+ return number.String(), nil
184+ }
185+
186+ return "", fmt.Errorf("unsupported webviewId format: %s", string(trimmed))
187+}
188+
125189 // SetWebView sets the WebView reference (needed for script injection)
126190 func (h *Handler) SetWebView(webView *webkit.WebView) {
127191 h.webView = webView
@@ -1,8 +1,10 @@
1 package messaging1 package messaging
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "encoding/json"6 "encoding/json"
7+ "fmt"
6 "log"8 "log"
7 "strconv"9 "strconv"
8 "time"10 "time"
@@ -75,8 +77,8 @@ func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
75 77
76 // Handle processes incoming script messages78 // Handle processes incoming script messages
77 func (h *Handler) Handle(payload string) {79 func (h *Handler) Handle(payload string) {
78- var msg Message80+ msg, err := parseIncomingMessage(payload)
79- if err := json.Unmarshal([]byte(payload), &msg); err != nil {81+ if err != nil {
80 log.Printf("[ERROR] Failed to unmarshal message: %v", err)82 log.Printf("[ERROR] Failed to unmarshal message: %v", err)
81 return83 return
82 }84 }
@@ -122,6 +124,68 @@ func (h *Handler) Handle(payload string) {
122 }124 }
123 }125 }
124 126
127+func parseIncomingMessage(payload string) (Message, error) {
128+ data := []byte(payload)
129+ var msg Message
130+ if err := json.Unmarshal(data, &msg); err == nil {
131+ return msg, nil
132+ } else {
133+ normalized, normErr := normalizeWebViewIDPayload(data)
134+ if normErr != nil {
135+ return Message{}, err
136+ }
137+
138+ if err := json.Unmarshal(normalized, &msg); err != nil {
139+ return Message{}, err
140+ }
141+
142+ return msg, nil
143+ }
144+}
145+
146+func normalizeWebViewIDPayload(data []byte) ([]byte, error) {
147+ var raw map[string]json.RawMessage
148+ if err := json.Unmarshal(data, &raw); err != nil {
149+ return nil, err
150+ }
151+
152+ rawID, ok := raw["webviewId"]
153+ if !ok {
154+ return nil, fmt.Errorf("webviewId missing in payload")
155+ }
156+
157+ normalizedID, err := parseWebViewIDRaw(rawID)
158+ if err != nil {
159+ return nil, err
160+ }
161+
162+ raw["webviewId"] = json.RawMessage(strconv.Quote(normalizedID))
163+
164+ return json.Marshal(raw)
165+}
166+
167+func parseWebViewIDRaw(raw json.RawMessage) (string, error) {
168+ trimmed := bytes.TrimSpace(raw)
169+ if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
170+ return "", nil
171+ }
172+
173+ if trimmed[0] == '"' {
174+ var id string
175+ if err := json.Unmarshal(trimmed, &id); err != nil {
176+ return "", err
177+ }
178+ return id, nil
179+ }
180+
181+ var number json.Number
182+ if err := json.Unmarshal(trimmed, &number); err == nil {
183+ return number.String(), nil
184+ }
185+
186+ return "", fmt.Errorf("unsupported webviewId format: %s", string(trimmed))
187+}
188+
125 // SetWebView sets the WebView reference (needed for script injection)189 // SetWebView sets the WebView reference (needed for script injection)
126 func (h *Handler) SetWebView(webView *webkit.WebView) {190 func (h *Handler) SetWebView(webView *webkit.WebView) {
127 h.webView = webView191 h.webView = webView
A internal/app/messaging/handler_test.go +27 -0
@@ -0,0 +1,27 @@
1+package messaging
2+
3+import "testing"
4+
5+func TestParseIncomingMessageAcceptsNumericWebViewID(t *testing.T) {
6+ payload := `{"type":"workspace","event":"focus","webviewId":42}`
7+ msg, err := parseIncomingMessage(payload)
8+ if err != nil {
9+ t.Fatalf("unexpected error parsing payload: %v", err)
10+ }
11+
12+ if msg.WebViewID != "42" {
13+ t.Fatalf("expected webviewId to be \"42\", got %q", msg.WebViewID)
14+ }
15+}
16+
17+func TestParseIncomingMessagePreservesStringWebViewID(t *testing.T) {
18+ payload := `{"type":"workspace","event":"focus","webviewId":"popup-123"}`
19+ msg, err := parseIncomingMessage(payload)
20+ if err != nil {
21+ t.Fatalf("unexpected error parsing payload: %v", err)
22+ }
23+
24+ if msg.WebViewID != "popup-123" {
25+ t.Fatalf("expected webviewId to remain popup-123, got %q", msg.WebViewID)
26+ }
27+}
@@ -0,0 +1,27 @@
1+package messaging
2+
3+import "testing"
4+
5+func TestParseIncomingMessageAcceptsNumericWebViewID(t *testing.T) {
6+ payload := `{"type":"workspace","event":"focus","webviewId":42}`
7+ msg, err := parseIncomingMessage(payload)
8+ if err != nil {
9+ t.Fatalf("unexpected error parsing payload: %v", err)
10+ }
11+
12+ if msg.WebViewID != "42" {
13+ t.Fatalf("expected webviewId to be \"42\", got %q", msg.WebViewID)
14+ }
15+}
16+
17+func TestParseIncomingMessagePreservesStringWebViewID(t *testing.T) {
18+ payload := `{"type":"workspace","event":"focus","webviewId":"popup-123"}`
19+ msg, err := parseIncomingMessage(payload)
20+ if err != nil {
21+ t.Fatalf("unexpected error parsing payload: %v", err)
22+ }
23+
24+ if msg.WebViewID != "popup-123" {
25+ t.Fatalf("expected webviewId to remain popup-123, got %q", msg.WebViewID)
26+ }
27+}

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