Repository navigation
Add debug.sidebar.simulate_drag for headless drag profiling #4771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70e7155
46566c2
70181a2
b847e04
3efad89
632b91a
926e9ec
18158f7
d441637
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9637,10 +9637,46 @@ private final class SidebarTabItemSettingsStore: ObservableObject { | |||||||||||||||||||||||||||
| final class SidebarDragState { | ||||||||||||||||||||||||||||
| var draggedTabId: UUID? | ||||||||||||||||||||||||||||
| var dropIndicator: SidebarDropIndicator? | ||||||||||||||||||||||||||||
| /// True while the `debug.sidebar.simulate_drag` debug-only V2 method is | ||||||||||||||||||||||||||||
| /// driving the drag state. The lifecycle observers honor this by not | ||||||||||||||||||||||||||||
| /// starting `SidebarDragFailsafeMonitor` (which would otherwise post a | ||||||||||||||||||||||||||||
| /// `mouse_up_failsafe` clear request immediately since no real mouse is | ||||||||||||||||||||||||||||
| /// pressed during simulation). DEBUG-only by convention; never set in | ||||||||||||||||||||||||||||
| /// release flows. | ||||||||||||||||||||||||||||
| var isSimulated: Bool = false | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| init() {} | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||
| /// Debug-only registry that exposes the live `SidebarDragState` of each | ||||||||||||||||||||||||||||
| /// mounted `VerticalTabsSidebar` keyed by `windowId`. The debug-socket | ||||||||||||||||||||||||||||
| /// `debug.sidebar.simulate_drag` handler reads from this so external | ||||||||||||||||||||||||||||
| /// profiling tools (e.g. the `profile-pr` skill driving `xctrace`) can | ||||||||||||||||||||||||||||
| /// generate deterministic drag-state mutations against the running app | ||||||||||||||||||||||||||||
| /// without HID synthesis. | ||||||||||||||||||||||||||||
| @MainActor | ||||||||||||||||||||||||||||
| enum SidebarDragStateRegistry { | ||||||||||||||||||||||||||||
| private static var statesByWindowId: [UUID: SidebarDragState] = [:] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static func register(windowId: UUID, dragState: SidebarDragState) { | ||||||||||||||||||||||||||||
| statesByWindowId[windowId] = dragState | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static func unregister(windowId: UUID) { | ||||||||||||||||||||||||||||
| statesByWindowId.removeValue(forKey: windowId) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static func state(forWindowId windowId: UUID) -> SidebarDragState? { | ||||||||||||||||||||||||||||
| statesByWindowId[windowId] | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static func registeredWindowIds() -> [UUID] { | ||||||||||||||||||||||||||||
| Array(statesByWindowId.keys) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Per-row drop-indicator visibility, computed by the parent from value | ||||||||||||||||||||||||||||
| /// inputs only. Takes UUIDs (not `Tab` objects or `SidebarDragState`) so it's | ||||||||||||||||||||||||||||
| /// trivially unit-testable and the row's view subtree never reads the | ||||||||||||||||||||||||||||
|
|
@@ -9967,6 +10003,14 @@ struct VerticalTabsSidebar: View { | |||||||||||||||||||||||||||
| modifierKeyMonitor.start() | ||||||||||||||||||||||||||||
| dragState.draggedTabId = nil | ||||||||||||||||||||||||||||
| dragState.dropIndicator = nil | ||||||||||||||||||||||||||||
| // Defensive reset: if a prior simulation died without running | ||||||||||||||||||||||||||||
| // its teardown (sidebar unmounted mid-loop, app crash, etc.) the | ||||||||||||||||||||||||||||
| // @State SidebarDragState could carry isSimulated=true into a | ||||||||||||||||||||||||||||
| // re-mount, which would silently bypass the real-drag failsafe. | ||||||||||||||||||||||||||||
| dragState.isSimulated = false | ||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||
| SidebarDragStateRegistry.register(windowId: windowId, dragState: dragState) | ||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||
|
Comment on lines
10003
to
+10013
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in d441637 — applied your suggested onAppear reset of — Claude Code |
||||||||||||||||||||||||||||
| SidebarDragLifecycleNotification.postStateDidChange( | ||||||||||||||||||||||||||||
| tabId: nil, | ||||||||||||||||||||||||||||
| reason: "sidebar_appear" | ||||||||||||||||||||||||||||
|
|
@@ -9978,6 +10022,12 @@ struct VerticalTabsSidebar: View { | |||||||||||||||||||||||||||
| dragFailsafeMonitor.stop() | ||||||||||||||||||||||||||||
| dragState.draggedTabId = nil | ||||||||||||||||||||||||||||
| dragState.dropIndicator = nil | ||||||||||||||||||||||||||||
| // Clear the simulator flag too so a re-mounted sidebar doesn't | ||||||||||||||||||||||||||||
| // inherit a stale bypass and skip the real-drag failsafe monitor. | ||||||||||||||||||||||||||||
| dragState.isSimulated = false | ||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||
| SidebarDragStateRegistry.unregister(windowId: windowId) | ||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||
| SidebarDragLifecycleNotification.postStateDidChange( | ||||||||||||||||||||||||||||
| tabId: nil, | ||||||||||||||||||||||||||||
| reason: "sidebar_disappear" | ||||||||||||||||||||||||||||
|
|
@@ -9992,8 +10042,15 @@ struct VerticalTabsSidebar: View { | |||||||||||||||||||||||||||
| cmuxDebugLog("sidebar.dragState.sidebar tab=\(debugShortSidebarTabId(newDraggedTabId))") | ||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||
| if newDraggedTabId != nil { | ||||||||||||||||||||||||||||
| dragFailsafeMonitor.start { | ||||||||||||||||||||||||||||
| SidebarDragLifecycleNotification.postClearRequest(reason: $0) | ||||||||||||||||||||||||||||
| // The failsafe monitor probes the real mouse-button state and | ||||||||||||||||||||||||||||
| // posts `mouse_up_failsafe` if no mouse is held down. That's | ||||||||||||||||||||||||||||
| // correct for HID-driven drags, but `debug.sidebar.simulate_drag` | ||||||||||||||||||||||||||||
| // drives the state without any mouse, so skip the monitor when | ||||||||||||||||||||||||||||
| // a simulated drag is in flight. | ||||||||||||||||||||||||||||
| if !dragState.isSimulated { | ||||||||||||||||||||||||||||
| dragFailsafeMonitor.start { | ||||||||||||||||||||||||||||
| SidebarDragLifecycleNotification.postClearRequest(reason: $0) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Localize newly introduced CLI-facing strings.
These new error/help/summary strings are hard-coded English. Route them through the project’s localization API and add matching catalog entries for all supported locales.
As per coding guidelines: “For production changes, fail when the diff violates
.github/review-bot-rules/full-internationalization.md: user-facing Swift text must useString(localized:defaultValue:)or an equivalent localized API with a matching translated string-catalog entry … for every locale.”Also applies to: 12112-12131
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Skipping — these are CLI dev-tool diagnostics (CLIError messages and per-command help text), not in-app UI strings. Existing CLI subcommands like reorder-workspace use the same hard-coded English pattern (CLI/cmux.swift:5885,5903). The CLAUDE.md localization rule targets user-facing UI strings; debug-only CLI scaffolding is consistent with the established CLI convention. Happy to revisit if you'd prefer the whole CLI surface go through xcstrings.
— Claude Code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@azooz2003-bit, that's a clear and well-reasoned distinction — CLI diagnostic/error strings and help text following the existing hard-coded English convention, separate from in-app UI localization through xcstrings. I'll record this to avoid raising the same flag on future CLI subcommands.Would you like me to resolve this comment?
✏️ Learnings added
🧠 Learnings used