detached a36e3e7

fix(workspace): keep stacked panes visible during popup splits

commit
a36e3e712b89519668ce318eb14048b40ea01444
author
bamen · · 2025-10-05 18:37 UTC
committer
bamen · · 2025-10-05 18:09 UTC
parent
81824f0
signature
unsigned
3 files · +46 -36
M gui/src/injected/main-world.ts +38 -34
@@ -628,7 +628,13 @@ function detectWindowType(features?: string | null): string {
628628
629629 // OAuth callback detection for auto-close
630630 const setupOAuthCallbackDetection = () => {
631+ let oauthCallbackHandled = false;
632+
631633 const detectOAuthCallback = () => {
634+ if (oauthCallbackHandled) {
635+ return;
636+ }
637+
632638 const url = window.location.href.toLowerCase();
633639
634640 // Check for OAuth callback patterns - be more specific to avoid false positives
@@ -647,69 +653,67 @@ function detectWindowType(features?: string | null): string {
647653 url.includes("googlepopupcallback");
648654
649655 if (isCallback) {
656+ oauthCallbackHandled = true;
650657 try {
651- // Determine which webview should be closed
658+ // Determine which WebView should close as part of this callback
652659 let targetWebViewId = window.__dumber_webview_id;
653660
654- // If this is a parent window detecting OAuth callback, find the associated popup
661+ // When the parent window detects the callback, look up its popup mapping
655662 const popupMappingKey = `popup_mapping_${window.__dumber_webview_id}`;
656- const popupMappingData = localStorage.getItem(popupMappingKey);
657-
658- if (popupMappingData) {
659- try {
663+ try {
664+ const popupMappingData = localStorage.getItem(popupMappingKey);
665+ if (popupMappingData) {
660666 const mapping = JSON.parse(popupMappingData);
661- // Check if mapping is recent (within last 60 seconds)
662667 const age = Date.now() - (mapping.timestamp || 0);
663668 if (age < 60000 && mapping.popupId) {
664669 targetWebViewId = mapping.popupId;
665670 console.log(
666671 `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`,
667672 );
668-
669- // Clean up the mapping since we're using it
670673 localStorage.removeItem(popupMappingKey);
671674 }
672- } catch (err) {
673- console.warn(
674- `[oauth-callback] Failed to parse popup mapping:`,
675- err,
676- );
677675 }
676+ } catch (err) {
677+ console.warn(
678+ `[oauth-callback] Failed to resolve popup mapping:`,
679+ err,
680+ );
678681 }
679682
680- const callbackData = {
681- url: window.location.href,
682- webviewId: targetWebViewId,
683- timestamp: Date.now(),
684- isOAuthCallback: true,
685- };
686-
687- localStorage.setItem(
688- `oauth_callback_${targetWebViewId}`,
689- JSON.stringify(callbackData),
690- );
691683 console.log(
692- `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`,
693- callbackData,
684+ `[oauth-callback] Detected OAuth callback, closing popup webview: ${targetWebViewId}`,
694685 );
695686
696- // Auto-cleanup sensitive OAuth data after 10 seconds
697- setTimeout(() => {
687+ // Send close request to backend for this popup webview
688+ const bridge = window.webkit?.messageHandlers?.dumber;
689+ if (bridge && typeof bridge.postMessage === "function") {
698690 try {
699- localStorage.removeItem(`oauth_callback_${targetWebViewId}`);
691+ const closeMessage = {
692+ type: "close-popup",
693+ webviewId: targetWebViewId,
694+ reason: "oauth-callback-success",
695+ timestamp: Date.now(),
696+ };
697+
700698 console.log(
701- `[oauth-callback] Cleaned up OAuth callback data from localStorage`,
699+ `[oauth-callback] Sending popup close request:`,
700+ closeMessage,
702701 );
702+ bridge.postMessage(JSON.stringify(closeMessage));
703703 } catch (err) {
704704 console.warn(
705- `[oauth-callback] Failed to cleanup OAuth callback data:`,
705+ `[oauth-callback] Failed to send popup close request:`,
706706 err,
707707 );
708708 }
709- }, 10000);
709+ } else {
710+ console.warn(
711+ `[oauth-callback] No webkit bridge available for popup close request`,
712+ );
713+ }
710714 } catch (err) {
711715 console.warn(
712- `[oauth-callback] Failed to store OAuth callback data:`,
716+ `[oauth-callback] Failed to handle OAuth callback:`,
713717 err,
714718 );
715719 }
@@ -628,7 +628,13 @@ function detectWindowType(features?: string | null): string {
628 628
629 // OAuth callback detection for auto-close629 // OAuth callback detection for auto-close
630 const setupOAuthCallbackDetection = () => {630 const setupOAuthCallbackDetection = () => {
631+ let oauthCallbackHandled = false;
632+
631 const detectOAuthCallback = () => {633 const detectOAuthCallback = () => {
634+ if (oauthCallbackHandled) {
635+ return;
636+ }
637+
632 const url = window.location.href.toLowerCase();638 const url = window.location.href.toLowerCase();
633 639
634 // Check for OAuth callback patterns - be more specific to avoid false positives640 // Check for OAuth callback patterns - be more specific to avoid false positives
@@ -647,69 +653,67 @@ function detectWindowType(features?: string | null): string {
647 url.includes("googlepopupcallback");653 url.includes("googlepopupcallback");
648 654
649 if (isCallback) {655 if (isCallback) {
656+ oauthCallbackHandled = true;
650 try {657 try {
651- // Determine which webview should be closed658+ // Determine which WebView should close as part of this callback
652 let targetWebViewId = window.__dumber_webview_id;659 let targetWebViewId = window.__dumber_webview_id;
653 660
654- // If this is a parent window detecting OAuth callback, find the associated popup661+ // When the parent window detects the callback, look up its popup mapping
655 const popupMappingKey = `popup_mapping_${window.__dumber_webview_id}`;662 const popupMappingKey = `popup_mapping_${window.__dumber_webview_id}`;
656- const popupMappingData = localStorage.getItem(popupMappingKey);663+ try {
657-664+ const popupMappingData = localStorage.getItem(popupMappingKey);
658- if (popupMappingData) {665+ if (popupMappingData) {
659- try {
660 const mapping = JSON.parse(popupMappingData);666 const mapping = JSON.parse(popupMappingData);
661- // Check if mapping is recent (within last 60 seconds)
662 const age = Date.now() - (mapping.timestamp || 0);667 const age = Date.now() - (mapping.timestamp || 0);
663 if (age < 60000 && mapping.popupId) {668 if (age < 60000 && mapping.popupId) {
664 targetWebViewId = mapping.popupId;669 targetWebViewId = mapping.popupId;
665 console.log(670 console.log(
666 `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`,671 `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`,
667 );672 );
668-
669- // Clean up the mapping since we're using it
670 localStorage.removeItem(popupMappingKey);673 localStorage.removeItem(popupMappingKey);
671 }674 }
672- } catch (err) {
673- console.warn(
674- `[oauth-callback] Failed to parse popup mapping:`,
675- err,
676- );
677 }675 }
676+ } catch (err) {
677+ console.warn(
678+ `[oauth-callback] Failed to resolve popup mapping:`,
679+ err,
680+ );
678 }681 }
679 682
680- const callbackData = {
681- url: window.location.href,
682- webviewId: targetWebViewId,
683- timestamp: Date.now(),
684- isOAuthCallback: true,
685- };
686-
687- localStorage.setItem(
688- `oauth_callback_${targetWebViewId}`,
689- JSON.stringify(callbackData),
690- );
691 console.log(683 console.log(
692- `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`,684+ `[oauth-callback] Detected OAuth callback, closing popup webview: ${targetWebViewId}`,
693- callbackData,
694 );685 );
695 686
696- // Auto-cleanup sensitive OAuth data after 10 seconds687+ // Send close request to backend for this popup webview
697- setTimeout(() => {688+ const bridge = window.webkit?.messageHandlers?.dumber;
689+ if (bridge && typeof bridge.postMessage === "function") {
698 try {690 try {
699- localStorage.removeItem(`oauth_callback_${targetWebViewId}`);691+ const closeMessage = {
692+ type: "close-popup",
693+ webviewId: targetWebViewId,
694+ reason: "oauth-callback-success",
695+ timestamp: Date.now(),
696+ };
697+
700 console.log(698 console.log(
701- `[oauth-callback] Cleaned up OAuth callback data from localStorage`,699+ `[oauth-callback] Sending popup close request:`,
700+ closeMessage,
702 );701 );
702+ bridge.postMessage(JSON.stringify(closeMessage));
703 } catch (err) {703 } catch (err) {
704 console.warn(704 console.warn(
705- `[oauth-callback] Failed to cleanup OAuth callback data:`,705+ `[oauth-callback] Failed to send popup close request:`,
706 err,706 err,
707 );707 );
708 }708 }
709- }, 10000);709+ } else {
710+ console.warn(
711+ `[oauth-callback] No webkit bridge available for popup close request`,
712+ );
713+ }
710 } catch (err) {714 } catch (err) {
711 console.warn(715 console.warn(
712- `[oauth-callback] Failed to store OAuth callback data:`,716+ `[oauth-callback] Failed to handle OAuth callback:`,
713 err,717 err,
714 );718 );
715 }719 }
M internal/app/browser/workspace_utils.go +7 -0
@@ -73,6 +73,13 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u
7373 func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) {
7474 _ = typeInfo // Type info kept for API compatibility but not used
7575 node.container = ptr
76+
77+ // Mark widget validity based on container presence so idle guards run for stacks too
78+ if ptr != 0 {
79+ node.widgetValid = true
80+ } else {
81+ node.widgetValid = false
82+ }
7683 }
7784
7885 // setTitleBar sets the titleBar widget pointer
@@ -73,6 +73,13 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u
73 func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) {73 func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) {
74 _ = typeInfo // Type info kept for API compatibility but not used74 _ = typeInfo // Type info kept for API compatibility but not used
75 node.container = ptr75 node.container = ptr
76+
77+ // Mark widget validity based on container presence so idle guards run for stacks too
78+ if ptr != 0 {
79+ node.widgetValid = true
80+ } else {
81+ node.widgetValid = false
82+ }
76 }83 }
77 84
78 // setTitleBar sets the titleBar widget pointer85 // setTitleBar sets the titleBar widget pointer
M pkg/webkit/workspace_widgets_cgo.go +1 -2
@@ -184,9 +184,8 @@ static void widget_remove_focus_controller(GtkWidget* widget, GtkEventController
184184
185185 tracker->is_attached = FALSE;
186186
187- // Remove from registry and unref controller to avoid leaks
187+ // Remove from registry; GTK drops the final reference when removing controller
188188 g_hash_table_remove(controller_registry, controller);
189- g_object_unref(controller);
190189 }
191190
192191 static GtkWidget* paned_get_start_child(GtkWidget* paned) {
@@ -184,9 +184,8 @@ static void widget_remove_focus_controller(GtkWidget* widget, GtkEventController
184 184
185 tracker->is_attached = FALSE;185 tracker->is_attached = FALSE;
186 186
187- // Remove from registry and unref controller to avoid leaks187+ // Remove from registry; GTK drops the final reference when removing controller
188 g_hash_table_remove(controller_registry, controller);188 g_hash_table_remove(controller_registry, controller);
189- g_object_unref(controller);
190 }189 }
191 190
192 static GtkWidget* paned_get_start_child(GtkWidget* paned) {191 static GtkWidget* paned_get_start_child(GtkWidget* paned) {

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