Repository navigation
Add customizable Jupyter directory mode #5222
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
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a7edf33
Add customizable directory browser tools
lawrencecchen 1e91d2f
Show shell directory tools in command palette
lawrencecchen 55d5118
Handle directory tool startup failures
lawrencecchen 3e5927d
Use uvx fallback for Jupyter directory tool
lawrencecchen 3038037
Fix Jupyter directory tool test unwrap
lawrencecchen 2faf33d
Show progress while launching directory tools
lawrencecchen 5839742
Show directory tool launch logs
lawrencecchen 8826491
Require approval before launching directory tools
lawrencecchen f1d3fce
Share directory tool launch log panel
lawrencecchen 8c3c801
Refine Jupyter launch controls
lawrencecchen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| import AppKit | ||
|
|
||
| final class DirectoryToolLaunchPanelController: NSObject, NSWindowDelegate { | ||
| private let panel: NSPanel | ||
| private let spinner: NSProgressIndicator | ||
| private let messageLabel: NSTextField | ||
| private let outputTextView: NSTextView | ||
| private let allowButton: NSButton | ||
| private let stopButton: NSButton | ||
| private let noOutputText: String | ||
| private let launchingMessage: String | ||
| private let launchingOutputText: String | ||
| private let onAllow: ((DirectoryToolLaunchPanelController) -> Void)? | ||
| private let onStop: () -> Void | ||
| private var isClosed = false | ||
| private var didAllow = false | ||
|
|
||
| init( | ||
| title: String, | ||
| initialMessage: String, | ||
| launchingMessage: String, | ||
| initialOutput: String, | ||
| requiresApproval: Bool, | ||
| onAllow: ((DirectoryToolLaunchPanelController) -> Void)?, | ||
| onStop: @escaping () -> Void | ||
| ) { | ||
| self.launchingMessage = launchingMessage | ||
| self.onAllow = onAllow | ||
| self.onStop = onStop | ||
| noOutputText = String( | ||
| localized: "directoryTool.launchProgress.noOutput", | ||
| defaultValue: "No output yet." | ||
| ) | ||
| launchingOutputText = String( | ||
| localized: "directoryTool.launchProgress.launching", | ||
| defaultValue: "Launching..." | ||
| ) | ||
|
|
||
| let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 520, height: 256)) | ||
|
|
||
| spinner = NSProgressIndicator() | ||
| spinner.style = .spinning | ||
| spinner.controlSize = .regular | ||
| spinner.isIndeterminate = true | ||
| spinner.isHidden = requiresApproval | ||
| if !requiresApproval { | ||
| spinner.startAnimation(nil) | ||
| } | ||
|
|
||
| let titleLabel = NSTextField(labelWithString: title) | ||
| titleLabel.font = .systemFont(ofSize: NSFont.systemFontSize, weight: .semibold) | ||
| titleLabel.lineBreakMode = .byTruncatingTail | ||
|
|
||
| messageLabel = NSTextField(labelWithString: initialMessage) | ||
| messageLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize) | ||
| messageLabel.textColor = .secondaryLabelColor | ||
| messageLabel.lineBreakMode = .byWordWrapping | ||
| messageLabel.maximumNumberOfLines = 2 | ||
|
|
||
| let headerStack = NSStackView(views: [spinner, titleLabel]) | ||
| headerStack.orientation = .horizontal | ||
| headerStack.alignment = .centerY | ||
| headerStack.spacing = 10 | ||
|
|
||
| outputTextView = NSTextView() | ||
| outputTextView.isEditable = false | ||
| outputTextView.isSelectable = true | ||
| outputTextView.drawsBackground = false | ||
| outputTextView.font = .monospacedSystemFont(ofSize: 11, weight: .regular) | ||
| outputTextView.textColor = .secondaryLabelColor | ||
| outputTextView.textContainerInset = NSSize(width: 8, height: 8) | ||
| outputTextView.string = initialOutput | ||
|
|
||
| let scrollView = NSScrollView() | ||
| scrollView.hasVerticalScroller = true | ||
| scrollView.drawsBackground = true | ||
| scrollView.backgroundColor = .textBackgroundColor.withAlphaComponent(0.65) | ||
| scrollView.borderType = .lineBorder | ||
| scrollView.documentView = outputTextView | ||
|
|
||
| allowButton = NSButton(title: String( | ||
| localized: "directoryTool.launchProgress.allow", | ||
| defaultValue: "Allow" | ||
| ), target: nil, action: nil) | ||
| allowButton.bezelStyle = .rounded | ||
| allowButton.keyEquivalent = "\r" | ||
| allowButton.isHidden = !requiresApproval | ||
|
|
||
| let stopButtonTitle: String | ||
| if requiresApproval { | ||
| stopButtonTitle = String( | ||
| localized: "directoryTool.launchProgress.cancel", | ||
| defaultValue: "Cancel" | ||
| ) | ||
| } else { | ||
| stopButtonTitle = String( | ||
| localized: "directoryTool.launchProgress.stop", | ||
| defaultValue: "Stop" | ||
| ) | ||
| } | ||
| stopButton = NSButton(title: stopButtonTitle, target: nil, action: nil) | ||
| stopButton.bezelStyle = .rounded | ||
|
|
||
| let footerStack = NSStackView(views: [NSView(), stopButton, allowButton]) | ||
| footerStack.orientation = .horizontal | ||
| footerStack.alignment = .centerY | ||
| footerStack.spacing = 8 | ||
|
|
||
| let stack = NSStackView(views: [headerStack, messageLabel, scrollView, footerStack]) | ||
| stack.orientation = .vertical | ||
| stack.alignment = .leading | ||
| stack.spacing = 10 | ||
| stack.translatesAutoresizingMaskIntoConstraints = false | ||
| contentView.addSubview(stack) | ||
|
|
||
| NSLayoutConstraint.activate([ | ||
| stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), | ||
| stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), | ||
| stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), | ||
| stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -14), | ||
| spinner.widthAnchor.constraint(equalToConstant: 18), | ||
| spinner.heightAnchor.constraint(equalToConstant: 18), | ||
| titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: stack.trailingAnchor), | ||
| messageLabel.widthAnchor.constraint(equalTo: stack.widthAnchor), | ||
| scrollView.widthAnchor.constraint(equalTo: stack.widthAnchor), | ||
| scrollView.heightAnchor.constraint(equalToConstant: 128) | ||
| ]) | ||
|
|
||
| panel = NSPanel( | ||
| contentRect: contentView.frame, | ||
| styleMask: [.titled, .closable, .nonactivatingPanel], | ||
| backing: .buffered, | ||
| defer: false | ||
| ) | ||
| panel.title = title | ||
| panel.contentView = contentView | ||
| panel.level = .floating | ||
| panel.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary] | ||
| panel.isReleasedWhenClosed = false | ||
|
|
||
| super.init() | ||
|
|
||
| panel.delegate = self | ||
| allowButton.target = self | ||
| allowButton.action = #selector(allow) | ||
| stopButton.target = self | ||
| stopButton.action = #selector(stop) | ||
| } | ||
|
|
||
| static func startingTitle(displayName: String) -> String { | ||
| let titleFormat = String( | ||
| localized: "directoryTool.launchProgress.title", | ||
| defaultValue: "Starting %@" | ||
| ) | ||
| return String(format: titleFormat, displayName) | ||
| } | ||
|
|
||
| static func commandPreview(command: String, directoryURL: URL) -> String { | ||
| """ | ||
| cd \(directoryURL.path) | ||
| \(command) | ||
| """ | ||
| } | ||
|
|
||
| func show(presentingWindow: NSWindow?) { | ||
| guard !isClosed else { return } | ||
| if let presentingWindow { | ||
| let windowFrame = presentingWindow.frame | ||
| let panelFrame = panel.frame | ||
| let origin = NSPoint( | ||
| x: windowFrame.midX - panelFrame.width / 2, | ||
| y: windowFrame.maxY - panelFrame.height - 72 | ||
| ) | ||
| panel.setFrameOrigin(origin) | ||
| } else { | ||
| panel.center() | ||
| } | ||
| panel.orderFrontRegardless() | ||
| } | ||
|
|
||
| func updateOutput(_ output: String) { | ||
| guard !isClosed else { return } | ||
| let trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| outputTextView.string = trimmed.isEmpty ? noOutputText : trimmed | ||
| outputTextView.scrollToEndOfDocument(nil) | ||
| } | ||
|
|
||
| func finish(message: String, stopTitle: String? = nil) { | ||
| guard !isClosed else { return } | ||
| spinner.stopAnimation(nil) | ||
| spinner.isHidden = true | ||
| messageLabel.stringValue = message | ||
| allowButton.isHidden = true | ||
| if let stopTitle { | ||
| stopButton.title = stopTitle | ||
| } | ||
| } | ||
|
|
||
| func close() { | ||
| guard !isClosed else { return } | ||
| isClosed = true | ||
| panel.close() | ||
| } | ||
|
|
||
| func windowWillClose(_ notification: Notification) { | ||
| if !isClosed { | ||
| onStop() | ||
| } | ||
| isClosed = true | ||
| } | ||
|
|
||
| @objc private func allow() { | ||
| guard !didAllow, !isClosed else { return } | ||
| didAllow = true | ||
| spinner.isHidden = false | ||
| spinner.startAnimation(nil) | ||
| messageLabel.stringValue = launchingMessage | ||
| outputTextView.string = launchingOutputText | ||
| allowButton.isHidden = true | ||
| stopButton.title = String( | ||
| localized: "directoryTool.launchProgress.stop", | ||
| defaultValue: "Stop" | ||
| ) | ||
| panel.contentView?.layoutSubtreeIfNeeded() | ||
| panel.displayIfNeeded() | ||
| onAllow?(self) | ||
| } | ||
|
|
||
| @objc private func stop() { | ||
| stopButton.isEnabled = false | ||
| onStop() | ||
| close() | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
After a user clicks Allow for a shell directory tool, closing the launch panel with the window close control no longer calls
onStopbecausedidAllowis true. In that scenario the child server keeps starting in the background and can still open a browser split even though the user dismissed the progress window; the close control should behave like Stop once the launch has begun.Useful? React with 👍 / 👎.