// One ordinary, non-modal window for everything profile-management related // — replaces what would otherwise be a string of separate popup alerts. // Opened from the menu bar's "Manage Profiles…" item or the standard // Cmd+, shortcut. Fixed-size (not user-resizable) — each tab reports its // own natural size via preferredContentSize, and NSTabViewController // resizes the window to match automatically, so it's never left showing // empty space sized for a different tab or state. import AppKit final class ManageWindowController: NSWindowController { static let shared = ManageWindowController() enum Tab: Int { case profiles = 0, settings = 1, about = 2 } private let profilesVC = ProfilesViewController() private let settingsVC = SettingsViewController() private let aboutVC = AboutViewController() private let tabs = NSTabViewController() // Called after a profile is added/removed so the caller can trigger an // immediate LiveState reconcile instead of waiting on the next // notification. var onProfilesChanged: (() -> Void)? { didSet { profilesVC.onChange = onProfilesChanged } } // Called when the window-tag style changes, so open tags can be // rebuilt in the new style immediately rather than at the next // launch/quit of a Claude window. var onOverlayStyleChanged: (() -> Void)? { didSet { settingsVC.onOverlayStyleChanged = onOverlayStyleChanged } } private init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 420, height: 260), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false) window.title = "shannoncoat" window.isReleasedWhenClosed = false window.center() profilesVC.title = "Profiles" settingsVC.title = "Settings" aboutVC.title = "About" tabs.addTabViewItem(NSTabViewItem(viewController: profilesVC)) tabs.addTabViewItem(NSTabViewItem(viewController: settingsVC)) tabs.addTabViewItem(NSTabViewItem(viewController: aboutVC)) window.contentViewController = tabs super.init(window: window) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show(tab: Tab? = nil) { NSApp.activate(ignoringOtherApps: true) if let tab { tabs.selectedTabViewItemIndex = tab.rawValue } window?.makeKeyAndOrderFront(nil) } func update(_ infos: [ProfileInfo]) { profilesVC.update(infos) } } // MARK: - Profiles tab private final class ProfilesViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate { var onChange: (() -> Void)? private var infos: [ProfileInfo] = [] private let tableView = NSTableView() private let addRemoveControl = NSSegmentedControl() private let detailStack = NSStackView() private let detailAppValue = NSTextField(labelWithString: "") private let detailCodeValue = NSTextField(labelWithString: "") private let detailAppCopyItem = NSMenuItem(title: "Copy Path", action: #selector(copyDetailPath(_:)), keyEquivalent: "") private let detailCodeCopyItem = NSMenuItem(title: "Copy Path", action: #selector(copyDetailPath(_:)), keyEquivalent: "") private let addForm = NSStackView() private let nameField = NSTextField() private let codeField = NSTextField() private let appField = NSTextField() private let errorLabel = NSTextField(labelWithString: "") private var outerStack: NSStackView! private var scrollHeightConstraint: NSLayoutConstraint! private static let margin: CGFloat = 20 private static let tableWidth: CGFloat = 340 private static let minVisibleRows = 3 private static let maxVisibleRows = 8 override func loadView() { view = NSView(frame: NSRect(x: 0, y: 0, width: 420, height: 260)) let column = NSTableColumn(identifier: .init("profile")) column.title = "Profile" column.width = 340 tableView.addTableColumn(column) tableView.headerView = nil tableView.dataSource = self tableView.delegate = self tableView.rowHeight = 24 tableView.usesAlternatingRowBackgroundColors = true tableView.menu = buildContextMenu() let scroll = NSScrollView() scroll.documentView = tableView scroll.hasVerticalScroller = true scroll.borderType = .bezelBorder scroll.translatesAutoresizingMaskIntoConstraints = false addRemoveControl.segmentCount = 2 addRemoveControl.trackingMode = .momentary addRemoveControl.segmentStyle = .smallSquare addRemoveControl.setImage(NSImage(systemSymbolName: "plus", accessibilityDescription: "Add Profile"), forSegment: 0) addRemoveControl.setImage( NSImage(systemSymbolName: "minus", accessibilityDescription: "Remove Profile"), forSegment: 1) addRemoveControl.setWidth(28, forSegment: 0) addRemoveControl.setWidth(28, forSegment: 1) addRemoveControl.setEnabled(false, forSegment: 1) addRemoveControl.setToolTip("Add Profile", forSegment: 0) addRemoveControl.setToolTip("Remove Profile", forSegment: 1) addRemoveControl.target = self addRemoveControl.action = #selector(addRemoveClicked(_:)) let buttonRow = NSStackView(views: [addRemoveControl, NSView()]) buttonRow.orientation = .horizontal buttonRow.translatesAutoresizingMaskIntoConstraints = false buildDetailStack() detailStack.isHidden = true detailStack.translatesAutoresizingMaskIntoConstraints = false buildAddForm() addForm.isHidden = true addForm.translatesAutoresizingMaskIntoConstraints = false let stack = NSStackView(views: [scroll, buttonRow, detailStack, addForm]) stack.orientation = .vertical stack.spacing = 12 stack.edgeInsets = NSEdgeInsets(top: Self.margin, left: Self.margin, bottom: Self.margin, right: Self.margin) stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) outerStack = stack scrollHeightConstraint = scroll.heightAnchor.constraint(equalToConstant: heightForRows(0)) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.topAnchor), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), scroll.widthAnchor.constraint(equalToConstant: Self.tableWidth), scrollHeightConstraint, // `scroll` is the one arranged subview with a real, absolute // width — everything else in this stack ties its width to // *that*, not to `stack` itself (whose own frame spans // edge-to-edge; edgeInsets only govern how the stack lays out // its children, not its own bounds). Left unconstrained, these // rows have no hard width anywhere in their constraint chain — // NSStackView doesn't fill them to the inset content width by // default, so they end up sized/positioned ambiguously (in // practice: flush against the window edge, sometimes wider // than the window). buttonRow.widthAnchor.constraint(equalTo: scroll.widthAnchor), detailStack.widthAnchor.constraint(equalTo: scroll.widthAnchor), addForm.widthAnchor.constraint(equalTo: scroll.widthAnchor), ]) updatePreferredSize() } // Sized to the actual row count (clamped to a sensible range) rather // than one fixed height — with only a couple of profiles that avoids a // big block of empty striped background below the last row; with many // it caps out and scrolls instead of taking over the window. private func heightForRows(_ count: Int) -> CGFloat { let rows = max(Self.minVisibleRows, min(count, Self.maxVisibleRows)) return CGFloat(rows) * tableView.rowHeight + 2 } // NSTabViewController watches its selected child's preferredContentSize // and resizes the window to match live — so recomputing this after // toggling the add form or the path-details row is what makes the // window grow/shrink to fit instead of sitting at one fixed size with // empty space in the states that don't need it. private func updatePreferredSize() { view.layoutSubtreeIfNeeded() preferredContentSize = outerStack.fittingSize } // A second, authoritative recompute after every real layout pass — // explicit calls from action handlers (below) update it eagerly for // responsiveness, but this is the one that can't be thrown off by // metrics not being fully resolved yet at the point an eager call runs. override func viewDidLayout() { super.viewDidLayout() preferredContentSize = outerStack.fittingSize } // Shows the selected profile's two dirs — the thing you'd otherwise // have to right-click "Reveal" to find out. Not `.isSelectable` — that // makes a label click-into-edit, and the field editor that appears // draws the *untruncated* string at its full intrinsic width, which // can extend past the window and get clipped by it rather than // wrapping or scrolling cleanly. A tooltip (full path) plus a // right-click "Copy Path" menu covers the same "get the value out" // need without that. private func buildDetailStack() { detailAppCopyItem.target = self detailCodeCopyItem.target = self let appMenu = NSMenu() appMenu.addItem(detailAppCopyItem) let codeMenu = NSMenu() codeMenu.addItem(detailCodeCopyItem) for (value, menu) in [(detailAppValue, appMenu), (detailCodeValue, codeMenu)] { value.textColor = .secondaryLabelColor value.font = .systemFont(ofSize: NSFont.smallSystemFontSize) value.lineBreakMode = .byTruncatingMiddle value.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) value.menu = menu } let appRow = NSStackView(views: [labeled("Claude Desktop:"), detailAppValue]) appRow.orientation = .horizontal appRow.spacing = 8 let codeRow = NSStackView(views: [labeled("Claude Code:"), detailCodeValue]) codeRow.orientation = .horizontal codeRow.spacing = 8 detailStack.orientation = .vertical detailStack.alignment = .leading detailStack.spacing = 4 [appRow, codeRow].forEach(detailStack.addArrangedSubview) appRow.widthAnchor.constraint(equalTo: detailStack.widthAnchor).isActive = true codeRow.widthAnchor.constraint(equalTo: detailStack.widthAnchor).isActive = true } private func buildAddForm() { nameField.placeholderString = "Profile name" nameField.delegate = self for field in [codeField, appField] { field.lineBreakMode = .byTruncatingMiddle field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) } codeField.placeholderString = "~/.shannoncoat/data//code" let codeChoose = NSButton(title: "Choose\u{2026}", target: self, action: #selector(chooseCodeDir)) let codeRow = NSStackView(views: [labeled("Claude Code:"), codeField, codeChoose]) codeRow.orientation = .horizontal codeRow.spacing = 8 appField.placeholderString = "~/.shannoncoat/data//app" let appChoose = NSButton(title: "Choose\u{2026}", target: self, action: #selector(chooseAppDir)) let appRow = NSStackView(views: [labeled("Claude Desktop:"), appField, appChoose]) appRow.orientation = .horizontal appRow.spacing = 8 errorLabel.textColor = .systemRed errorLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize) errorLabel.maximumNumberOfLines = 0 errorLabel.lineBreakMode = .byWordWrapping let createButton = NSButton(title: "Create", target: self, action: #selector(createProfile)) createButton.keyEquivalent = "\r" let cancelButton = NSButton(title: "Cancel", target: self, action: #selector(toggleAddForm)) let formButtonRow = NSStackView(views: [NSView(), cancelButton, createButton]) formButtonRow.orientation = .horizontal formButtonRow.spacing = 8 let divider = NSBox() divider.boxType = .separator addForm.orientation = .vertical addForm.alignment = .leading addForm.spacing = 10 [divider, nameField, codeRow, appRow, errorLabel, formButtonRow].forEach(addForm.addArrangedSubview) divider.widthAnchor.constraint(equalTo: addForm.widthAnchor).isActive = true nameField.widthAnchor.constraint(equalTo: addForm.widthAnchor).isActive = true codeRow.widthAnchor.constraint(equalTo: addForm.widthAnchor).isActive = true appRow.widthAnchor.constraint(equalTo: addForm.widthAnchor).isActive = true formButtonRow.widthAnchor.constraint(equalTo: addForm.widthAnchor).isActive = true } // Fixed-width, trailing-aligned label so the fields in a two-row form // (differing label lengths: "Claude Code:" vs "Claude Desktop:") start // at the same x position instead of a ragged left edge. private func labeled(_ text: String) -> NSTextField { let field = NSTextField(labelWithString: text) field.font = .systemFont(ofSize: NSFont.systemFontSize) field.textColor = .secondaryLabelColor field.setContentHuggingPriority(.required, for: .horizontal) field.alignment = .right field.widthAnchor.constraint(equalToConstant: 104).isActive = true return field } func update(_ infos: [ProfileInfo]) { self.infos = infos tableView.reloadData() scrollHeightConstraint.constant = heightForRows(infos.count) refreshForSelection() } private func refreshForSelection() { let row = tableView.selectedRow guard infos.indices.contains(row) else { addRemoveControl.setEnabled(false, forSegment: 1) detailStack.isHidden = true updatePreferredSize() return } let profile = infos[row].profile addRemoveControl.setEnabled(profile.name != "default", forSegment: 1) detailAppValue.stringValue = profile.appDir detailAppValue.toolTip = profile.appDir detailAppCopyItem.representedObject = profile.appDir detailCodeValue.stringValue = profile.codeDir detailCodeValue.toolTip = profile.codeDir detailCodeCopyItem.representedObject = profile.codeDir detailStack.isHidden = false updatePreferredSize() } @objc private func copyDetailPath(_ sender: NSMenuItem) { guard let path = sender.representedObject as? String else { return } NSPasteboard.general.clearContents() NSPasteboard.general.setString(path, forType: .string) } // MARK: NSTableViewDataSource / Delegate func numberOfRows(in tableView: NSTableView) -> Int { infos.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let identifier = NSUserInterfaceItemIdentifier("profileCell") let info = infos[row] let cell: NSTableCellView if let reused = tableView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView { cell = reused } else { cell = NSTableCellView() cell.identifier = identifier let imageView = NSImageView() let textField = NSTextField(labelWithString: "") imageView.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false cell.addSubview(imageView) cell.addSubview(textField) cell.imageView = imageView cell.textField = textField NSLayoutConstraint.activate([ imageView.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 6), imageView.centerYAnchor.constraint(equalTo: cell.centerYAnchor), imageView.widthAnchor.constraint(equalToConstant: 10), imageView.heightAnchor.constraint(equalToConstant: 10), textField.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 8), textField.trailingAnchor.constraint(lessThanOrEqualTo: cell.trailingAnchor, constant: -6), textField.centerYAnchor.constraint(equalTo: cell.centerYAnchor), ]) } cell.imageView?.image = ProfileColor.dotImage(for: info.profile.name, dimmed: !info.running) let text = NSMutableAttributedString( string: info.profile.name, attributes: [.font: NSFont.systemFont(ofSize: NSFont.systemFontSize)]) text.append(NSAttributedString( string: " \(info.running ? "Running" : "Stopped")", attributes: [ .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), .foregroundColor: NSColor.secondaryLabelColor, ])) cell.textField?.attributedStringValue = text return cell } func tableViewSelectionDidChange(_ notification: Notification) { refreshForSelection() } // MARK: NSTextFieldDelegate — live default-path preview as the name is typed func controlTextDidChange(_ obj: Notification) { guard (obj.object as? NSTextField) === nameField else { return } let name = nameField.stringValue.isEmpty ? "" : nameField.stringValue codeField.placeholderString = "~/.shannoncoat/data/\(name)/code" appField.placeholderString = "~/.shannoncoat/data/\(name)/app" } // MARK: Actions @objc private func addRemoveClicked(_ sender: NSSegmentedControl) { switch sender.selectedSegment { case 0: toggleAddForm() case 1: removeSelected() default: break } } @objc private func toggleAddForm() { addForm.isHidden.toggle() if !addForm.isHidden { nameField.stringValue = "" codeField.stringValue = "" appField.stringValue = "" errorLabel.stringValue = "" view.window?.makeFirstResponder(nameField) } updatePreferredSize() } @objc private func chooseCodeDir() { choose(into: codeField) } @objc private func chooseAppDir() { choose(into: appField) } private func choose(into field: NSTextField) { let panel = NSOpenPanel() panel.canChooseDirectories = true panel.canChooseFiles = false panel.allowsMultipleSelection = false guard let window = view.window else { return } panel.beginSheetModal(for: window) { response in if response == .OK, let url = panel.url { field.stringValue = url.path } } } @objc private func createProfile() { let name = nameField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) let code = codeField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) let app = appField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) do { try ProfileStore.create(name: name, codeDir: code.isEmpty ? nil : code, appDir: app.isEmpty ? nil : app) toggleAddForm() onChange?() } catch { errorLabel.stringValue = (error as? LocalizedError)?.errorDescription ?? "\(error)" updatePreferredSize() } } @objc private func removeSelected() { let row = tableView.selectedRow guard infos.indices.contains(row) else { return } let info = infos[row] guard info.profile.name != "default", let window = view.window else { return } let alert = NSAlert() alert.alertStyle = .warning alert.messageText = "Delete profile \u{201C}\(info.profile.name)\u{201D}?" alert.informativeText = info.running ? "This profile is currently running \u{2014} deleting it will quit Claude first. Its Claude Desktop and Claude Code data stay on disk; only the profile pointer is removed." : "Its Claude Desktop and Claude Code data stay on disk; only the profile pointer is removed." alert.addButton(withTitle: "Delete") alert.addButton(withTitle: "Cancel") alert.buttons.first?.hasDestructiveAction = true alert.beginSheetModal(for: window) { [weak self] response in guard response == .alertFirstButtonReturn else { return } let finish = { try? ProfileStore.delete(info.profile.name) self?.onChange?() } if info.running, let pid = info.pid { ClaudeControl.quit(pid: pid) { _ in DispatchQueue.main.async(execute: finish) } } else { finish() } } } private func buildContextMenu() -> NSMenu { let menu = NSMenu() let revealCode = NSMenuItem(title: "Reveal Code Folder", action: #selector(revealCodeDir), keyEquivalent: "") revealCode.target = self let revealApp = NSMenuItem( title: "Reveal Desktop Folder", action: #selector(revealAppDir), keyEquivalent: "") revealApp.target = self menu.addItem(revealCode) menu.addItem(revealApp) return menu } private func clickedInfo() -> ProfileInfo? { let row = tableView.clickedRow guard infos.indices.contains(row) else { return nil } return infos[row] } @objc private func revealCodeDir() { guard let info = clickedInfo() else { return } NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: info.profile.codeDir) } @objc private func revealAppDir() { guard let info = clickedInfo() else { return } NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: info.profile.appDir) } } // MARK: - Settings tab private final class SettingsViewController: NSViewController { private let autoUpdateCheckbox = NSButton( checkboxWithTitle: "Automatically check for updates", target: nil, action: nil) private let launchAtLoginCheckbox = NSButton(checkboxWithTitle: "Launch at Login", target: nil, action: nil) private let checkNowButton = NSButton(title: "Check for Updates\u{2026}", target: nil, action: nil) private let tagStyleLabel = NSTextField(labelWithString: "Window tag:") private let tagStylePopUp = NSPopUpButton(frame: .zero, pullsDown: false) private var tagStyleRow: NSStackView! private var stack: NSStackView! var onOverlayStyleChanged: (() -> Void)? // Order matches the popup's items, so the selected index maps straight // onto a style without a title-string comparison. private static let tagStyles: [(title: String, style: WindowOverlayStyle)] = [ ("Name chip", .chip), ("Colored dot", .dot), ] override func loadView() { view = NSView(frame: NSRect(x: 0, y: 0, width: 420, height: 150)) tagStylePopUp.addItems(withTitles: Self.tagStyles.map(\.title)) tagStylePopUp.selectItem(at: Self.tagStyles.firstIndex { $0.style == WindowOverlayPosition.style } ?? 0) tagStylePopUp.target = self tagStylePopUp.action = #selector(changeTagStyle) tagStyleRow = NSStackView(views: [tagStyleLabel, tagStylePopUp]) tagStyleRow.orientation = .horizontal tagStyleRow.spacing = 8 launchAtLoginCheckbox.target = self launchAtLoginCheckbox.action = #selector(toggleLaunchAtLogin) launchAtLoginCheckbox.state = LaunchAtLogin.isEnabled ? .on : .off autoUpdateCheckbox.target = self autoUpdateCheckbox.action = #selector(toggleAutoUpdate) autoUpdateCheckbox.state = UpdateChecker.automaticCheckEnabled ? .on : .off checkNowButton.target = self checkNowButton.action = #selector(checkNow) checkNowButton.bezelStyle = .rounded stack = NSStackView(views: [launchAtLoginCheckbox, tagStyleRow, autoUpdateCheckbox, checkNowButton]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 14 stack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.topAnchor), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), stack.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) updatePreferredSize() } override func viewDidLayout() { super.viewDidLayout() updatePreferredSize() } // NOT `stack.fittingSize` for the width — that was measured observing // it clip a checkbox's label by a consistent amount regardless of // when it's read (loadView vs. viewDidLayout: same result), so this // isn't a measure-too-early timing issue, it's `NSStackView.fittingSize` // itself undercounting a checkbox's true rendered width. Asking each // control directly for its own `intrinsicContentSize` instead — which // is what AutoLayout itself uses to size an unconstrained control — // sidesteps whatever the stack's own aggregation is getting wrong. private func updatePreferredSize() { // Same reasoning for the tag-style row: summed from its two // controls plus the stack spacing rather than trusting the row's // own fittingSize. let rowWidth = tagStyleLabel.intrinsicContentSize.width + tagStylePopUp.intrinsicContentSize.width + tagStyleRow.spacing let widest = [launchAtLoginCheckbox, autoUpdateCheckbox, checkNowButton] .map(\.intrinsicContentSize.width) .max().map { max($0, rowWidth) } ?? rowWidth preferredContentSize = NSSize(width: widest + 40, height: stack.fittingSize.height) } @objc private func changeTagStyle() { let index = tagStylePopUp.indexOfSelectedItem guard Self.tagStyles.indices.contains(index) else { return } WindowOverlayPosition.style = Self.tagStyles[index].style onOverlayStyleChanged?() } @objc private func toggleLaunchAtLogin() { do { if launchAtLoginCheckbox.state == .on { try LaunchAtLogin.enable() } else { try LaunchAtLogin.disable() } } catch { launchAtLoginCheckbox.state = LaunchAtLogin.isEnabled ? .on : .off NSAlert(error: error).runModal() } } @objc private func toggleAutoUpdate() { UpdateChecker.automaticCheckEnabled = autoUpdateCheckbox.state == .on } @objc private func checkNow() { UpdateChecker.checkManually { result in DispatchQueue.main.async { let alert = NSAlert() switch result { case .upToDate: alert.messageText = "You're up to date." alert.runModal() case .updateAvailable(let version, let url): alert.messageText = "shannoncoat \(version) is available" alert.addButton(withTitle: "View Release") alert.addButton(withTitle: "Later") if alert.runModal() == .alertFirstButtonReturn { NSWorkspace.shared.open(url) } case .failed: alert.alertStyle = .warning alert.messageText = "Couldn't check for updates." alert.runModal() } } } } } // MARK: - About tab private let projectAuthor = "bdeshi" private let projectHomepage = "https://github.com/bdeshi/shannoncoat" private final class AboutViewController: NSViewController { private static let contentWidth: CGFloat = 340 override func loadView() { view = NSView(frame: NSRect(x: 0, y: 0, width: 420, height: 150)) // Centered within a full-width wrapper rather than centered in the // stack directly — the stack is leading-aligned (for the text // rows below), and its own per-arranged-subview alignment // constraint on the icon would conflict with an added centering // constraint on the icon itself. let icon = NSImageView() icon.image = NSApp.applicationIconImage ?? NSImage(named: NSImage.applicationIconName) icon.translatesAutoresizingMaskIntoConstraints = false let iconContainer = NSView() iconContainer.translatesAutoresizingMaskIntoConstraints = false iconContainer.addSubview(icon) NSLayoutConstraint.activate([ icon.widthAnchor.constraint(equalToConstant: 96), icon.heightAnchor.constraint(equalToConstant: 96), icon.topAnchor.constraint(equalTo: iconContainer.topAnchor), icon.bottomAnchor.constraint(equalTo: iconContainer.bottomAnchor), icon.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor), ]) let description = NSTextField(wrappingLabelWithString: "Runs multiple Claude Desktop profiles side by side, each paired with its own isolated Claude Code config.") description.font = .systemFont(ofSize: NSFont.systemFontSize) let authorField = NSTextField(labelWithString: "Author: \(projectAuthor)") let sourceField = NSTextField(labelWithString: "Source: \(projectHomepage)") let versionField = NSTextField(labelWithString: "Version: \(UpdateChecker.displayVersion)") for field in [authorField, sourceField, versionField] { field.textColor = .secondaryLabelColor field.font = .systemFont(ofSize: NSFont.smallSystemFontSize) } let stack = NSStackView(views: [iconContainer, description, authorField, sourceField, versionField]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 10 stack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.topAnchor), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), description.widthAnchor.constraint(equalToConstant: Self.contentWidth), iconContainer.widthAnchor.constraint(equalTo: description.widthAnchor), ]) view.layoutSubtreeIfNeeded() preferredContentSize = NSSize(width: Self.contentWidth + 40, height: stack.fittingSize.height) } }