Repository navigation
Add dedicated create-PR window, load branch tip history on demand, and relayout diff on resize #58
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
bba7d6c
77fa4bc
6b0abeb
45c4e8a
6cb9b2a
8f75103
9464526
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 |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| import AppKit | ||
| import Combine | ||
| import SwiftUI | ||
|
|
||
| @MainActor | ||
| final class CreatePullRequestWindowController: NSWindowController, NSWindowDelegate, ObservableObject { | ||
| private var onClose: (() -> Void)? | ||
| @Published private(set) var cancelRequestID = UUID() | ||
|
|
||
| init() { | ||
| super.init(window: nil) | ||
| } | ||
|
|
||
| required init?(coder: NSCoder) { | ||
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| func show<Content: View>(repositoryName: String, content: Content, onClose: @escaping () -> Void) { | ||
| close() | ||
| self.onClose = onClose | ||
|
|
||
| let screen = NSApp.keyWindow?.screen ?? NSScreen.main | ||
| let visibleFrame = screen?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800) | ||
| let size = NSSize( | ||
| width: min(1080, visibleFrame.width - 40), | ||
| height: min(720, visibleFrame.height - 80) | ||
| ) | ||
| let window = NSWindow( | ||
| contentRect: NSRect(origin: .zero, size: size), | ||
| styleMask: [.titled, .closable, .miniaturizable, .resizable], | ||
| backing: .buffered, | ||
| defer: false | ||
| ) | ||
| window.title = "Create Pull Request — \(repositoryName)" | ||
| window.isReleasedWhenClosed = false | ||
| window.tabbingMode = .disallowed | ||
| window.contentMinSize = NSSize(width: min(820, size.width), height: min(520, size.height)) | ||
| window.delegate = self | ||
|
|
||
| let hostingView = NSHostingView(rootView: content) | ||
| hostingView.sizingOptions = [] | ||
| window.contentView = hostingView | ||
| window.setContentSize(size) | ||
| window.setFrameOrigin(NSPoint( | ||
| x: visibleFrame.midX - window.frame.width / 2, | ||
| y: visibleFrame.midY - window.frame.height / 2 | ||
| )) | ||
| self.window = window | ||
| showWindow(nil) | ||
| window.makeKeyAndOrderFront(nil) | ||
| } | ||
|
|
||
| override func close() { | ||
| window?.close() | ||
| } | ||
|
|
||
| func activateIfPresented() -> Bool { | ||
| guard let window else { return false } | ||
| if window.isMiniaturized { | ||
| window.deminiaturize(nil) | ||
| } | ||
| window.makeKeyAndOrderFront(nil) | ||
| return true | ||
| } | ||
|
|
||
| func windowShouldClose(_ sender: NSWindow) -> Bool { | ||
| cancelRequestID = UUID() | ||
| return false | ||
| } | ||
|
|
||
| func windowWillClose(_ notification: Notification) { | ||
| let callback = onClose | ||
| onClose = nil | ||
| window = nil | ||
| callback?() | ||
| } | ||
| } | ||
|
|
||
| struct CreatePullRequestWindowContent: View { | ||
| @ObservedObject var controller: PullRequestController | ||
| @ObservedObject var windowController: CreatePullRequestWindowController | ||
| let seed: PullRequestDraftSeed | ||
| let repositoryURL: URL | ||
| let authorizeAction: () async -> Bool | ||
| let onGenerateText: PullRequestAIGenerationAction | ||
| let onCancel: () -> Void | ||
| let onCreate: (PullRequestDraft) async -> Void | ||
| @State private var isSubmitting = false | ||
|
|
||
| var body: some View { | ||
| CreatePullRequestView( | ||
| seed: seed, | ||
| repositoryURL: repositoryURL, | ||
| isSubmitting: isSubmitting || controller.isPerformingAction, | ||
| changedFileCount: controller.createDraftChangedFileCount, | ||
| isLoadingChanges: controller.isLoadingCreateDraftChanges, | ||
| changesErrorMessage: controller.createDraftChangesErrorMessage, | ||
| participants: controller.createDraftParticipants, | ||
| isLoadingParticipants: controller.isLoadingCreateDraftParticipants, | ||
| participantsErrorMessage: controller.createDraftParticipantsErrorMessage, | ||
| loadSourceBranches: { query in | ||
| await controller.loadCreateDraftSourceBranches(query: query) | ||
| }, | ||
| loadTargetBranches: { query in | ||
| await controller.loadCreateDraftTargetBranches(query: query) | ||
| }, | ||
| onGenerateText: onGenerateText, | ||
| onCancel: onCancel, | ||
| onBranchesChanged: { sourceBranch, targetBranch in | ||
| Task { | ||
| guard await authorizeAction() else { return } | ||
| if let targetBranch { | ||
| await controller.loadCreateDraftChanges( | ||
| sourceBranch: sourceBranch, | ||
| targetBranch: targetBranch | ||
| ) | ||
| } else { | ||
| controller.resetCreateDraftChanges() | ||
| } | ||
| } | ||
| }, | ||
| onCreate: { draft in | ||
| guard !isSubmitting else { return } | ||
| isSubmitting = true | ||
| Task { | ||
| await onCreate(draft) | ||
| isSubmitting = false | ||
| } | ||
| }, | ||
| isPresentedInWindow: true, | ||
| cancelRequestID: windowController.cancelRequestID | ||
| ) | ||
| .overlay { | ||
| if isSubmitting || controller.isPerformingAction { | ||
| ZStack { | ||
| Color(nsColor: .windowBackgroundColor) | ||
| .opacity(0.72) | ||
| ProgressView("Creating pull request…") | ||
| .controlSize(.large) | ||
| } | ||
| .ignoresSafeArea() | ||
| } | ||
| } | ||
| .alert("Pull Request", isPresented: detailErrorPresented) { | ||
| Button("OK", role: .cancel) { | ||
| controller.detailErrorMessage = nil | ||
| } | ||
| } message: { | ||
| Text(controller.detailErrorMessage ?? "Could not create pull request.") | ||
| } | ||
| } | ||
|
|
||
| private var detailErrorPresented: Binding<Bool> { | ||
| Binding( | ||
| get: { controller.detailErrorMessage != nil }, | ||
| set: { isPresented in | ||
| if !isPresented { | ||
| controller.detailErrorMessage = nil | ||
| } | ||
| } | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,7 +106,13 @@ struct HistoryScreen: View { | |
| .onChange(of: baseBranch) { _, value in listModel.baseBranch = value } | ||
| .onChange(of: selectedBranch) { _, value in | ||
| listModel.selectedBranch = value | ||
| listModel.selectBranchTipIfPossible() | ||
| guard let value else { return } | ||
| Task { | ||
| let selectedLoadedTip = await listModel.selectReferenceTipIfLoaded(value) | ||
| guard !Task.isCancelled, !selectedLoadedTip, | ||
| selectedBranch == value, branchFilter == .all else { return } | ||
| branchFilter = .branch(value) | ||
|
Comment on lines
+112
to
+114
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep the branch filter unchanged during an active search. If 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| .onChange(of: searchText) { _, value in listModel.setSearchText(value) } | ||
| .onChange(of: historyLoadSizeRaw) { _, value in | ||
|
|
||
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clear the previous draft seed before preparation can fail.
prepareCreatePullRequestdoes not resetcreateDraftSeedwhen it begins. Suppose a draft seed remains from an earlier session, and a later preparation fails with an error such as "Connect Account...". The callers inMainWindowView+PullRequestActions.swiftcheckcreateDraftSeed == nilbefore they show the error. That check fails because the old seed is still set, so the callers open the window with stale data from the earlier repository or branch. The callers also never show the error.dismissCreatePullRequestclears the seed when the window closes, but the seed survives any path that skips that call, such as a successfulcreatePullRequestfollowed by a failed refresh. Reset the draft state at the start of preparation so that each failure path is reported reliably.Proposed fix
activeRepositoryURL = repositoryURL detailErrorMessage = nil + dismissCreatePullRequest()📝 Committable suggestion
🤖 Prompt for AI Agents