Skip to content

Add Safari DevTools diagnostic logging - #1161

Closed
lawrencecchen wants to merge 2 commits into
mainfrom
task-safari-devtools-debug-logs
Closed

lawrencecchen wants to merge 2 commits into
mainfrom
task-safari-devtools-debug-logs

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add high-signal Safari DevTools diagnostics around inspector lookup, selector availability, restore retries, and show/hide paths
  • add portal attachment and visibility logs so repros show whether DevTools is failing in the inspector bridge or portal lifecycle
  • log missing portal mappings and refresh/hide skips that were previously silent

Testing

  • git diff --check
  • ./scripts/reload.sh --tag safari-devtools-logs failed in this worktree because the current machine is missing the Metal toolchain component needed to build GhosttyKit.xcframework; after reusing the existing framework from the base checkout, tagged xcodebuild progressed through the modified files and produced the app bundle, but LaunchServices failed to open the tagged app with error -54

Task

  • User request: "safari devtools is broken now, add lots of debug logs and ill reproduce"

Summary by cubic

Adds detailed, DEBUG-only diagnostics for Safari DevTools and the portal lifecycle to trace why the inspector fails to show or attach. Also ignores repeated DevTools shortcuts to prevent accidental rapid toggles.

  • New Features
    • DevTools: richer logs for toggle/show/hide/console, selector checks, restore retries (schedule/fire/cancel), abort/skip reasons, and a consolidated diagnostics summary (state, geometry, inspector presence/visibility/selectors, portal binding).
    • Portal: logs for bind/update/hide/refresh with begin/skip reasons (missing entry/window mapping/container hidden), plus visibility/z changes and refresh begin.
    • Shortcut handling: ignore key-repeat for DevTools shortcuts and log repeat snapshots.
    • Logs gated by #if DEBUG; shortcut repeat handling applies in all builds.

Written for commit 6dbf582. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Prevents repeated key-repeat activations of developer tools and console shortcuts so a held key no longer triggers multiple toggles.
  • Chores

    • Expanded DEBUG-only diagnostic logging across browser panel, panel view, and portal flows to provide richer state summaries for developers without changing user-facing behavior.

@vercel

vercel Bot commented Mar 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 10, 2026 9:47pm

@coderabbitai

coderabbitai Bot commented Mar 10, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds DEBUG-only diagnostic logging across browser portal and panel codepaths, introduces a portalEntryMissing computed flag used during portal binding decisions, and guards repeated DevTools/console shortcut repeats; no public API/signature changes or functional behavior changes beyond diagnostics and early-return on repeat key events.

Changes

Cohort / File(s) Summary
Window Portal Diagnostics
Sources/BrowserWindowPortal.swift
Inserted #if DEBUG logging in early-return and skip branches (missing entries, missing window/anchor mappings, skipped actions). Logs identifiers (webViewId, anchor, container) and skip reasons without changing control flow or state mutations.
Panel Diagnostics Helpers
Sources/Panels/BrowserPanel.swift
Added private DEBUG helper functions to assemble inspector/portal/diagnostic summaries and a standardized logDeveloperToolsDebug formatter; replaced/extended many DEBUG logs across DevTools toggle, console, restore/retry, and lifecycle paths to include richer diagnostics.
Panel View Portal Binding
Sources/Panels/BrowserPanelView.swift
Introduced portalEntryMissing computed flag and used it when deciding shouldBindNow and when calling portal.update; replaced simpler debug payloads with aggregated diagnostics in DevTools and portal update logs.
Shortcut Repeat Guards
Sources/AppDelegate.swift
Added guards to ignore repeated key events for toggleBrowserDeveloperTools and showBrowserJavaScriptConsole; DEBUG logs repeatIgnored snapshot and returns early for repeated events.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 With whiskers twitching, I hop and peep,
I log the portals while others sleep,
A missing entry I gently flag,
I whisper diagnostics—soft and wag,
Hooray for traces that help you leap!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Safari DevTools diagnostic logging' accurately summarizes the main change—introducing DEBUG-only diagnostic logs for Safari DevTools and portal lifecycle operations.
Description check ✅ Passed The description includes a clear summary of changes (diagnostics for inspector, portal, and shortcuts), testing notes, and context from the user's task request, covering all critical areas.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-safari-devtools-debug-logs

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds comprehensive #if DEBUG diagnostic logging across the Safari DevTools code path — covering inspector object lookup, selector availability, show/hide/toggle outcomes, restore retry scheduling/cancellation, and portal binding/visibility lifecycle — to help reproduce and diagnose a Safari DevTools breakage. All production paths are unchanged; every new log statement is guarded by #if DEBUG.

Key changes:

  • New logDeveloperToolsDebug(event:details:) helper in BrowserPanel stamps every devtools event with a full debugDeveloperToolsDiagnosticsSummary() snapshot (state, geometry, inspector selectors, and portal registry state).
  • New debugDeveloperToolsInspectorSummary() and debugDeveloperToolsPortalSummary() helpers surface private-API selector availability and portal-entry state in one structured line.
  • BrowserWindowPortal's previously-silent guard … else { return } paths now emit keyed log lines explaining why the operation was skipped.
  • portalEntryMissing is hoisted above the portal host claiming/releasing block in BrowserPanelView.updateViewState; while functionally safe (other shouldBindNow conditions cover the cross-window case), the early computation means entryMissing in the portal.update log can reflect "anchor had no window yet" rather than "binding genuinely absent", potentially reducing the diagnostic precision of the new field.
  • debugDeveloperToolsDiagnosticsSummary() invokes cmuxInspectorObject() twice per call (once in debugDeveloperToolsStateSummary, once in debugDeveloperToolsInspectorSummary); in fast retry/portal-update bursts this adds redundant private-API overhead in debug builds.

Confidence Score: 4/5

  • Safe to merge — all new code is #if DEBUG-only with no production logic changes, but one subtle semantic shift in portalEntryMissing is worth addressing before relying on the new entryMissing log field for diagnosis.
  • Every new statement is wrapped in #if DEBUG, so release builds are unaffected. The only non-trivial structural change is the hoisting of portalEntryMissing before installPortalAnchorView, which is functionally safe but reduces the accuracy of the diagnostic field it was moved to support. The double cmuxInspectorObject() per log call is a debug-build overhead concern only. Overall the PR accomplishes its stated goal of adding high-signal diagnostics with minimal risk.
  • Sources/Panels/BrowserPanelView.swift — the portalEntryMissing hoisting deserves a second look to ensure the entryMissing diagnostic field is trustworthy in practice.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Adds logDeveloperToolsDebug(event:details:) helper that stamps every devtools event with the full debugDeveloperToolsDiagnosticsSummary() (state + geometry + inspector selectors + portal snapshot); expands toggleDeveloperTools, showDeveloperTools, showDeveloperToolsConsole, hideDeveloperTools, syncDeveloperToolsPreferenceFromInspector, restoreDeveloperToolsAfterAttachIfNeeded, scheduleDeveloperToolsRestoreRetry, and cancelDeveloperToolsRestoreRetry with fine-grained #if DEBUG log points at every abort/skip/retry path. Minor: debugDeveloperToolsDiagnosticsSummary calls cmuxInspectorObject() twice per invocation (once via debugDeveloperToolsStateSummary, once via debugDeveloperToolsInspectorSummary).
Sources/Panels/BrowserPanelView.swift Expands portal.update log details and adds toggleDevTools.failed diagnostic; moves portalEntryMissing computation from inside the if host.window != nil, portalHostAccepted block (after installPortalAnchorView) to before all portal host claiming/releasing logic. The hoisted position means isWebView short-circuits on anchorView.window == nil for new-attach cases, making the entryMissing debug field conflate "anchor not yet in window" with "binding absent in registry".
Sources/BrowserWindowPortal.swift Adds #if DEBUG diagnostic logs at every silent return path in updateEntryVisibility, hideWebView, forceRefreshWebView, and all three static registry wrappers (updateEntryVisibility, hide, refresh, bind); no logic changes to the non-debug paths.

Sequence Diagram

sequenceDiagram
    participant UI as BrowserPanelView
    participant BP as BrowserPanel
    participant WKI as WKWebView Inspector
    participant PR as BrowserWindowPortalRegistry

    UI->>BP: toggleDeveloperTools()
    BP-->>BP: log toggle.begin [diagnosticsSummary]
    BP->>WKI: cmuxInspectorObject()
    alt inspector nil
        BP-->>BP: log toggle.abort reason=no_inspector
    else inspector found
        BP->>WKI: responds(to: show/close)?
        alt selector missing
            BP-->>BP: log toggle.abort reason=missing_selector
        else selector available
            BP->>WKI: cmuxCallVoid(show/close)
            BP-->>BP: log toggle.end [targetVisible, selector]
            BP-->>BP: async tick: log toggle.tick
        end
    end

    UI->>BP: showDeveloperTools()
    BP->>WKI: cmuxInspectorObject()
    alt no inspector
        BP-->>BP: log show.abort reason=no_inspector
    else
        BP->>WKI: cmuxCallVoid(show)
        BP->>WKI: isVisible?
        alt visible after show
            BP->>BP: cancelDeveloperToolsRestoreRetry()
            BP-->>BP: log retry.cancel
        else not visible
            BP->>BP: scheduleDeveloperToolsRestoreRetry()
            BP-->>BP: log retry.schedule [attempt, delay]
            note over BP: after delay…
            BP-->>BP: log retry.fire [attempt]
            BP->>BP: restoreDeveloperToolsAfterAttachIfNeeded()
        end
        BP-->>BP: log show.end [visibleBefore, visibleAfter]
    end

    UI->>BP: updateViewState (portal)
    BP->>PR: isWebView(boundTo: anchorView) [portalEntryMissing check]
    note over BP,PR: ⚠ captured before installPortalAnchorView
    UI->>PR: bind / updateEntryVisibility / hide
    alt missing_window_mapping
        PR-->>PR: log portal.*.skip reason=missing_window_mapping
    else portal found
        PR->>PR: update entry
        PR-->>PR: log portal.visibility / portal.hide / portal.refresh
    end
    BP-->>BP: log portal.update [full diagnosticsSummary + entryMissing]
Loading

Last reviewed commit: 7977484

let activeSearchOverlay = coordinator.desiredPortalVisibleInUI ? searchOverlay : nil
let portalAnchorView = panel.portalAnchorView
let portalHideReason = !isCurrentPaneOwner ? "lostPaneOwnership" : "hidden"
let portalEntryMissing = !BrowserWindowPortalRegistry.isWebView(webView, boundTo: portalAnchorView)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

portalEntryMissing captured before installPortalAnchorView changes anchor window

portalEntryMissing is now computed at this line, before installPortalAnchorView(portalAnchorView, in: host) runs at line ~4370. BrowserWindowPortalRegistry.isWebView returns false immediately when anchorView.window == nil, so on any first-attach (or re-attach after a detach) where portalAnchorView is not yet in a window, portalEntryMissing will be true — not because no portal entry exists in the registry, but simply because the window lookup short-circuits. The old code computed this variable inside the if host.window != nil, portalHostAccepted block, after installPortalAnchorView, which correctly queried "does the registry have a binding for this anchor's current window?"

The functional impact on shouldBindNow is benign (a stale true just makes bind run, which is correct), but the entryMissing=1 field in the portal.update diagnostic log now conflates "anchor had no window yet" with "binding was genuinely absent", reducing the signal quality of the log you're adding this PR to improve. Consider moving it back inside the if host.window != nil, portalHostAccepted block at line ~4441, or at minimum computing it after installPortalAnchorView so the window lookup is valid.

Comment on lines +3709 to +3715
private func logDeveloperToolsDebug(event: String, details: String? = nil) {
var line = "browser.devtools event=\(event) panel=\(id.uuidString.prefix(5)) \(debugDeveloperToolsDiagnosticsSummary())"
if let details, !details.isEmpty {
line += " \(details)"
}
dlog(line)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmuxInspectorObject() called twice per log event via debugDeveloperToolsDiagnosticsSummary

debugDeveloperToolsDiagnosticsSummary() composes four sub-summaries. Two of them independently invoke cmuxInspectorObject():

  • debugDeveloperToolsStateSummary() (line 3808): let inspector = webView.cmuxInspectorObject() == nil ? 0 : 1
  • debugDeveloperToolsInspectorSummary() (line 3761): let inspector = webView.cmuxInspectorObject()

Since logDeveloperToolsDebug calls debugDeveloperToolsDiagnosticsSummary() unconditionally on every event — including the high-frequency retry loop and every portal.update tick — this means every single log line incurs two private-API calls to obtain the inspector object. In addition, debugDeveloperToolsGeometrySummary() performs a full subview-tree walk (debugInspectorSubviewCount) on each call.

This is #if DEBUG-only so production is unaffected, but repro sessions can generate many events in quick succession (retries, view-state updates), and the redundant work makes the debug build noticeably heavier. Caching the inspector result inside debugDeveloperToolsDiagnosticsSummary and sharing it across the four sub-summaries would eliminate the duplication and the double lookup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 7692-7697: The logs for repeat-suppressed shortcuts are
incorrectly marking the action as handled; update the calls to
logDeveloperToolsShortcutSnapshot (the branches checking event.isARepeat) to
record didHandle: false (or remove the handled flag) so repeats are logged as
ignored rather than "handled"; make the same change for both occurrences (the
toggle.repeatIgnored branch and the other repeat-suppression branch around the
second occurrence) so diagnostic traces correctly reflect that the action did
not run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c71f826-80b5-4915-b802-cfcd37f06983

📥 Commits

Reviewing files that changed from the base of the PR and between 7977484 and 6dbf582.

📒 Files selected for processing (1)
  • Sources/AppDelegate.swift

Comment thread Sources/AppDelegate.swift
Comment on lines +7692 to +7697
if event.isARepeat {
#if DEBUG
logDeveloperToolsShortcutSnapshot(phase: "toggle.repeatIgnored", event: event, didHandle: true)
#endif
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't log ignored repeats as handled.

These branches intentionally skip the action, but the new trace records handled=1. In the surrounding post/tick logs, handled means the browser action actually ran, so this will make repeat-suppression look like a successful inspector/console open and muddy the diagnostics.

🔧 Proposed fix
             if event.isARepeat {
 `#if` DEBUG
-                logDeveloperToolsShortcutSnapshot(phase: "toggle.repeatIgnored", event: event, didHandle: true)
+                logDeveloperToolsShortcutSnapshot(phase: "toggle.repeatIgnored", event: event)
 `#endif`
                 return true
             }
@@
             if event.isARepeat {
 `#if` DEBUG
-                logDeveloperToolsShortcutSnapshot(phase: "console.repeatIgnored", event: event, didHandle: true)
+                logDeveloperToolsShortcutSnapshot(phase: "console.repeatIgnored", event: event)
 `#endif`
                 return true
             }

Also applies to: 7713-7718

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 7692 - 7697, The logs for
repeat-suppressed shortcuts are incorrectly marking the action as handled;
update the calls to logDeveloperToolsShortcutSnapshot (the branches checking
event.isARepeat) to record didHandle: false (or remove the handled flag) so
repeats are logged as ignored rather than "handled"; make the same change for
both occurrences (the toggle.repeatIgnored branch and the other
repeat-suppression branch around the second occurrence) so diagnostic traces
correctly reflect that the action did not run.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview — 6dbf5823 Deployed Mar 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants