← back to Exo
feat(app): open Share Bug Report in a dedicated window (#2003)
1606e63816adf64884a2f4405084c5a9df7d9f0b · 2026-04-30 13:05:07 +0100 · Alex Cheema
## Summary
- Adds a top-level **Share Bug Report…** menu item to the macOS popover
(between *Check for Updates* and *Quit*) with SF Symbol `ladybug`.
- Clicking it opens a dedicated resizable `NSWindow` ("Send a Bug
Report") that hosts the prompting / sending / success / failure flow.
- Removes the description-less duplicate from Settings → Debug Info, and
the dead `debugSection` it nominally lived behind.
## Why
PR #1959 added a user-description prompt to the bug-report flow, but its
trigger lived inside `ContentView.debugSection` — a view that's defined
but never rendered in the body. The path users actually hit was
`SettingsView.sendBugReportButton`, which called
`BugReportService.sendReport(isManual: true)` without ever passing
`userDescription`. So the description prompt was unreachable in the
built app.
## Approach
Per Apple HIG, an action that requires further input before completing
should open a dialog, not transform the menu inline. So:
- Add a top-level menu entry that ends in `…` (HIG: ellipsis indicates
"further input required").
- Move the prompting/sending/success/failure state machine into a
standalone `BugReportWindowController` modeled after the existing
`SettingsWindowController`.
- Single-instance window with frame-autosave name, sensible
`contentMinSize`, resizable, native button layout (`.cancelAction` /
`.defaultAction` keyboard shortcuts), light/dark-mode-correct
`.textBackgroundColor` and `.separatorColor`.
- Auto-focus the description field on open. `Try Again` from failure,
`Open GitHub Issue` + `Done` from success.
## Files
- `app/EXO/EXO/Views/BugReportWindowController.swift` (new) — controller
+ view.
- `app/EXO/EXO/EXOApp.swift` — wire `BugReportWindowController` as a
`@StateObject` and inject as environment object.
- `app/EXO/EXO/ContentView.swift` — replace inline state machine with
menu item that calls `bugReportWindowController.open()`. Remove
now-unused state, helpers, and dead `debugSection`.
- `app/EXO/EXO/Views/SettingsView.swift` — remove duplicate
`sendBugReportButton`, `sendBugReport()`, and related `@State`. Section
"Debug Info" keeps Thunderbolt / interface / RDMA info.
`BugReportService` is unchanged.
## Test plan
- [ ] Open the menu-bar popover → confirm **Share Bug Report…** appears
between *Check for Updates* and *Quit*, with a ladybug icon.
- [ ] Click it → a window titled "Send a Bug Report" appears, centered,
with the description editor focused.
- [ ] Resize the window → size persists across re-opens (frame
autosave).
- [ ] Type a description, press Return → upload succeeds, success card
with **Open GitHub Issue** + **Done** appears.
- [ ] Click **Open GitHub Issue** → browser opens with the description
pre-filled into the issue template.
- [ ] Send with empty description → upload still succeeds.
- [ ] Press Esc from the prompting state → window closes.
- [ ] On failure (e.g., offline) → error card with **Try Again** +
**Close** appears; Try Again returns to the editor with the description
preserved.
- [ ] Open the Settings window → Debug Info section is unchanged except
the Send Bug Report button is gone.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M app/EXO/EXO/ContentView.swiftM app/EXO/EXO/EXOApp.swiftA app/EXO/EXO/Views/BugReportWindowController.swiftM app/EXO/EXO/Views/SettingsView.swift
Diff
commit 1606e63816adf64884a2f4405084c5a9df7d9f0b
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Thu Apr 30 13:05:07 2026 +0100
feat(app): open Share Bug Report in a dedicated window (#2003)
## Summary
- Adds a top-level **Share Bug Report…** menu item to the macOS popover
(between *Check for Updates* and *Quit*) with SF Symbol `ladybug`.
- Clicking it opens a dedicated resizable `NSWindow` ("Send a Bug
Report") that hosts the prompting / sending / success / failure flow.
- Removes the description-less duplicate from Settings → Debug Info, and
the dead `debugSection` it nominally lived behind.
## Why
PR #1959 added a user-description prompt to the bug-report flow, but its
trigger lived inside `ContentView.debugSection` — a view that's defined
but never rendered in the body. The path users actually hit was
`SettingsView.sendBugReportButton`, which called
`BugReportService.sendReport(isManual: true)` without ever passing
`userDescription`. So the description prompt was unreachable in the
built app.
## Approach
Per Apple HIG, an action that requires further input before completing
should open a dialog, not transform the menu inline. So:
- Add a top-level menu entry that ends in `…` (HIG: ellipsis indicates
"further input required").
- Move the prompting/sending/success/failure state machine into a
standalone `BugReportWindowController` modeled after the existing
`SettingsWindowController`.
- Single-instance window with frame-autosave name, sensible
`contentMinSize`, resizable, native button layout (`.cancelAction` /
`.defaultAction` keyboard shortcuts), light/dark-mode-correct
`.textBackgroundColor` and `.separatorColor`.
- Auto-focus the description field on open. `Try Again` from failure,
`Open GitHub Issue` + `Done` from success.
## Files
- `app/EXO/EXO/Views/BugReportWindowController.swift` (new) — controller
+ view.
- `app/EXO/EXO/EXOApp.swift` — wire `BugReportWindowController` as a
`@StateObject` and inject as environment object.
- `app/EXO/EXO/ContentView.swift` — replace inline state machine with
menu item that calls `bugReportWindowController.open()`. Remove
now-unused state, helpers, and dead `debugSection`.
- `app/EXO/EXO/Views/SettingsView.swift` — remove duplicate
`sendBugReportButton`, `sendBugReport()`, and related `@State`. Section
"Debug Info" keeps Thunderbolt / interface / RDMA info.
`BugReportService` is unchanged.
## Test plan
- [ ] Open the menu-bar popover → confirm **Share Bug Report…** appears
between *Check for Updates* and *Quit*, with a ladybug icon.
- [ ] Click it → a window titled "Send a Bug Report" appears, centered,
with the description editor focused.
- [ ] Resize the window → size persists across re-opens (frame
autosave).
- [ ] Type a description, press Return → upload succeeds, success card
with **Open GitHub Issue** + **Done** appears.
- [ ] Click **Open GitHub Issue** → browser opens with the description
pre-filled into the issue template.
- [ ] Send with empty description → upload still succeeds.
- [ ] Press Esc from the prompting state → window closes.
- [ ] On failure (e.g., offline) → error card with **Try Again** +
**Close** appears; Try Again returns to the editor with the description
preserved.
- [ ] Open the Settings window → Debug Info section is unchanged except
the Send Bug Report button is gone.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
app/EXO/EXO/ContentView.swift | 235 +--------------------
app/EXO/EXO/EXOApp.swift | 3 +
app/EXO/EXO/Views/BugReportWindowController.swift | 240 ++++++++++++++++++++++
app/EXO/EXO/Views/SettingsView.swift | 46 -----
4 files changed, 251 insertions(+), 273 deletions(-)
diff --git a/app/EXO/EXO/ContentView.swift b/app/EXO/EXO/ContentView.swift
index c30df992..30a4fc59 100644
--- a/app/EXO/EXO/ContentView.swift
+++ b/app/EXO/EXO/ContentView.swift
@@ -16,22 +16,13 @@ struct ContentView: View {
@EnvironmentObject private var updater: SparkleUpdater
@EnvironmentObject private var thunderboltBridgeService: ThunderboltBridgeService
@EnvironmentObject private var settingsWindowController: SettingsWindowController
+ @EnvironmentObject private var bugReportWindowController: BugReportWindowController
@State private var focusedNode: NodeViewModel?
@State private var deletingInstanceIDs: Set<String> = []
@State private var showAllNodes = false
@State private var showAllInstances = false
@State private var baseURLCopied = false
@State private var showAdvanced = false
- @State private var showDebugInfo = false
- private enum BugReportPhase: Equatable {
- case idle
- case prompting
- case sending(String)
- case success(String)
- case failure(String)
- }
- @State private var bugReportPhase: BugReportPhase = .idle
- @State private var bugReportUserDescription: String = ""
@State private var uninstallInProgress = false
@State private var pendingNamespace: String = ""
@State private var pendingHFToken: String = ""
@@ -294,6 +285,13 @@ struct ContentView: View {
) {
updater.checkForUpdates()
}
+ HoverButton(
+ title: "Share Bug Report…",
+ tint: .primary,
+ trailingSystemImage: "ladybug"
+ ) {
+ bugReportWindowController.open()
+ }
.padding(.bottom, 8)
HoverButton(title: "Quit", tint: .secondary) {
controller.stop()
@@ -477,40 +475,6 @@ struct ContentView: View {
}
}
- private var debugSection: some View {
- VStack(alignment: .leading, spacing: 4) {
- HoverButton(
- title: "Debug Info",
- tint: .primary,
- trailingSystemImage: showDebugInfo ? "chevron.up" : "chevron.down",
- small: true
- ) {
- showDebugInfo.toggle()
- }
- if showDebugInfo {
- VStack(alignment: .leading, spacing: 4) {
- Text("Version: \(buildTag)")
- .font(.caption2)
- .foregroundColor(.secondary)
- Text("Commit: \(buildCommit)")
- .font(.caption2)
- .foregroundColor(.secondary)
- Text(thunderboltStatusText)
- .font(.caption2)
- .foregroundColor(thunderboltStatusColor)
- clusterThunderboltBridgeView
- interfaceIpList
- rdmaStatusView
- sendBugReportButton
- .padding(.top, 6)
- }
- .padding(.leading, 8)
- .transition(.opacity)
- }
- }
- .animation(.easeInOut(duration: 0.25), value: showDebugInfo)
- }
-
private var rdmaStatusView: some View {
let rdmaStatuses = stateService.latestSnapshot?.nodeRdmaCtl ?? [:]
let localNodeId = stateService.localNodeId
@@ -559,127 +523,6 @@ struct ContentView: View {
}
}
- private var sendBugReportButton: some View {
- VStack(alignment: .leading, spacing: 6) {
- switch bugReportPhase {
- case .idle:
- Button {
- bugReportPhase = .prompting
- bugReportUserDescription = ""
- } label: {
- HStack {
- Text("Send Bug Report")
- .font(.caption)
- .fontWeight(.semibold)
- Spacer()
- }
- .padding(.vertical, 6)
- .padding(.horizontal, 8)
- .background(
- RoundedRectangle(cornerRadius: 6)
- .fill(Color.accentColor.opacity(0.12))
- )
- }
- .buttonStyle(.plain)
-
- case .prompting:
- VStack(alignment: .leading, spacing: 6) {
- VStack(alignment: .leading, spacing: 2) {
- Text("Tell us what went wrong (optional)")
- .font(.caption2)
- .foregroundColor(.secondary)
- Text(
- "A quick description of what you were doing and what happened helps us track down the bug for you."
- )
- .font(.caption2)
- .foregroundColor(.secondary)
- .opacity(0.8)
- .fixedSize(horizontal: false, vertical: true)
- }
- TextEditor(text: $bugReportUserDescription)
- .font(.caption2)
- .frame(height: 60)
- .overlay(
- RoundedRectangle(cornerRadius: 4)
- .stroke(Color.secondary.opacity(0.3), lineWidth: 1)
- )
- HStack(spacing: 8) {
- Button("Send") {
- Task {
- await sendBugReport()
- }
- }
- .font(.caption2)
- .buttonStyle(.borderedProminent)
- .controlSize(.small)
- Button("Cancel") {
- bugReportPhase = .idle
- }
- .font(.caption2)
- .buttonStyle(.bordered)
- .controlSize(.small)
- }
- }
- .padding(8)
- .background(
- RoundedRectangle(cornerRadius: 6)
- .fill(Color.accentColor.opacity(0.06))
- )
-
- case .sending(let message):
- HStack(spacing: 6) {
- ProgressView()
- .scaleEffect(0.6)
- Text(message)
- .font(.caption2)
- .foregroundColor(.secondary)
- }
-
- case .success(let message):
- VStack(alignment: .leading, spacing: 6) {
- Text(message)
- .font(.caption2)
- .foregroundColor(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- Button {
- openGitHubIssue()
- } label: {
- HStack(spacing: 4) {
- Image(systemName: "arrow.up.right.square")
- .imageScale(.small)
- Text("Create GitHub Issue")
- .font(.caption2)
- }
- }
- .buttonStyle(.bordered)
- .controlSize(.small)
- Button("Done") {
- bugReportPhase = .idle
- bugReportUserDescription = ""
- }
- .font(.caption2)
- .buttonStyle(.plain)
- .foregroundColor(.secondary)
- }
-
- case .failure(let message):
- VStack(alignment: .leading, spacing: 4) {
- Text(message)
- .font(.caption2)
- .foregroundColor(.red)
- .fixedSize(horizontal: false, vertical: true)
- Button("Dismiss") {
- bugReportPhase = .idle
- }
- .font(.caption2)
- .buttonStyle(.plain)
- .foregroundColor(.secondary)
- }
- }
- }
- .animation(.easeInOut(duration: 0.2), value: bugReportPhase)
- }
-
private var processToggleBinding: Binding<Bool> {
Binding(
get: {
@@ -720,61 +563,6 @@ struct ContentView: View {
)
}
- private func sendBugReport() async {
- bugReportPhase = .sending("Collecting logs...")
- let service = BugReportService()
- let description = bugReportUserDescription.trimmingCharacters(in: .whitespacesAndNewlines)
- do {
- let outcome = try await service.sendReport(
- isManual: true,
- userDescription: description.isEmpty ? nil : description
- )
- if outcome.success {
- bugReportPhase = .success(outcome.message)
- } else {
- bugReportPhase = .failure(outcome.message)
- }
- } catch {
- bugReportPhase = .failure(error.localizedDescription)
- }
- }
-
- private func openGitHubIssue() {
- let description = bugReportUserDescription.trimmingCharacters(in: .whitespacesAndNewlines)
-
- var bodyParts: [String] = []
- bodyParts.append("## Describe the bug")
- bodyParts.append("")
- if !description.isEmpty {
- bodyParts.append(description)
- } else {
- bodyParts.append("A clear and concise description of what the bug is.")
- }
- bodyParts.append("")
- bodyParts.append("## Environment")
- bodyParts.append("")
- bodyParts.append("- macOS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)")
- bodyParts.append("- EXO Version: \(buildTag) (\(buildCommit))")
- bodyParts.append("")
- bodyParts.append("## Additional context")
- bodyParts.append("")
- bodyParts.append("A bug report with diagnostic logs was submitted via the app.")
-
- let body = bodyParts.joined(separator: "\n")
-
- var components = URLComponents(string: "https://github.com/exo-explore/exo/issues/new")!
- components.queryItems = [
- URLQueryItem(name: "template", value: "bug_report.md"),
- URLQueryItem(name: "title", value: "[BUG] "),
- URLQueryItem(name: "body", value: body),
- URLQueryItem(name: "labels", value: "bug"),
- ]
-
- if let url = components.url {
- NSWorkspace.shared.open(url)
- }
- }
-
private func showUninstallConfirmationAlert() {
let alert = NSAlert()
alert.messageText = "Uninstall EXO"
@@ -857,13 +645,6 @@ struct ContentView: View {
}
}
- private var buildTag: String {
- Bundle.main.infoDictionary?["EXOBuildTag"] as? String ?? "unknown"
- }
-
- private var buildCommit: String {
- Bundle.main.infoDictionary?["EXOBuildCommit"] as? String ?? "unknown"
- }
}
private struct HoverButton: View {
diff --git a/app/EXO/EXO/EXOApp.swift b/app/EXO/EXO/EXOApp.swift
index 45a6cdda..5757b0eb 100644
--- a/app/EXO/EXO/EXOApp.swift
+++ b/app/EXO/EXO/EXOApp.swift
@@ -22,6 +22,7 @@ struct EXOApp: App {
@StateObject private var updater: SparkleUpdater
@StateObject private var thunderboltBridgeService: ThunderboltBridgeService
@StateObject private var settingsWindowController: SettingsWindowController
+ @StateObject private var bugReportWindowController: BugReportWindowController
private let terminationObserver: TerminationObserver
private let firstLaunchPopout = FirstLaunchPopout()
private let ciContext = CIContext(options: nil)
@@ -46,6 +47,7 @@ struct EXOApp: App {
let thunderboltBridge = ThunderboltBridgeService(clusterStateService: service)
_thunderboltBridgeService = StateObject(wrappedValue: thunderboltBridge)
_settingsWindowController = StateObject(wrappedValue: SettingsWindowController())
+ _bugReportWindowController = StateObject(wrappedValue: BugReportWindowController())
enableLaunchAtLoginIfNeeded()
// Install LaunchDaemon to disable Thunderbolt Bridge on startup (prevents network loops)
NetworkSetupHelper.promptAndInstallIfNeeded()
@@ -66,6 +68,7 @@ struct EXOApp: App {
.environmentObject(updater)
.environmentObject(thunderboltBridgeService)
.environmentObject(settingsWindowController)
+ .environmentObject(bugReportWindowController)
} label: {
menuBarIcon
.onReceive(controller.$isFirstLaunchReady) { ready in
diff --git a/app/EXO/EXO/Views/BugReportWindowController.swift b/app/EXO/EXO/Views/BugReportWindowController.swift
new file mode 100644
index 00000000..853976c0
--- /dev/null
+++ b/app/EXO/EXO/Views/BugReportWindowController.swift
@@ -0,0 +1,240 @@
+import AppKit
+import SwiftUI
+
+/// Manages a standalone window for the bug-report flow.
+/// Ensures only one instance exists and brings it to front on repeated opens.
+@MainActor
+final class BugReportWindowController: ObservableObject {
+ private var window: NSWindow?
+
+ func open() {
+ if let existing = window, existing.isVisible {
+ existing.makeKeyAndOrderFront(nil)
+ NSApp.activate()
+ return
+ }
+
+ let view = BugReportView(onDismiss: { [weak self] in
+ self?.window?.close()
+ })
+
+ let hostingView = NSHostingView(rootView: view)
+
+ let newWindow = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 480, height: 380),
+ styleMask: [.titled, .closable, .resizable],
+ backing: .buffered,
+ defer: false
+ )
+ newWindow.title = "Send a Bug Report"
+ newWindow.contentView = hostingView
+ newWindow.contentMinSize = NSSize(width: 420, height: 320)
+ newWindow.center()
+ newWindow.setFrameAutosaveName("ExoBugReportWindow")
+ newWindow.isReleasedWhenClosed = false
+ newWindow.makeKeyAndOrderFront(nil)
+ NSApp.activate()
+
+ window = newWindow
+ }
+}
+
+private struct BugReportView: View {
+ fileprivate enum Phase: Equatable {
+ case prompting
+ case sending(String)
+ case success(String)
+ case failure(String)
+ }
+
+ let onDismiss: () -> Void
+
+ @State private var phase: Phase = .prompting
+ @State private var userDescription: String = ""
+ @FocusState private var descriptionFocused: Bool
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ switch phase {
+ case .prompting:
+ promptingView
+ case .sending(let message):
+ sendingView(message: message)
+ case .success(let message):
+ successView(message: message)
+ case .failure(let message):
+ failureView(message: message)
+ }
+ }
+ .padding(20)
+ .frame(minWidth: 420, minHeight: 320)
+ .animation(.easeInOut(duration: 0.2), value: phase)
+ .onAppear { descriptionFocused = true }
+ }
+
+ private var promptingView: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Tell us what went wrong")
+ .font(.headline)
+ Text(
+ "A short description of what you were doing and what happened helps us track down the bug. This is optional — diagnostic logs will be sent either way."
+ )
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+
+ TextEditor(text: $userDescription)
+ .font(.body)
+ .scrollContentBackground(.hidden)
+ .padding(6)
+ .frame(minHeight: 120)
+ .background(
+ RoundedRectangle(cornerRadius: 6)
+ .fill(Color(nsColor: .textBackgroundColor))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 6)
+ .strokeBorder(Color(nsColor: .separatorColor), lineWidth: 1)
+ )
+ .focused($descriptionFocused)
+
+ HStack {
+ Spacer()
+ Button("Cancel") { onDismiss() }
+ .keyboardShortcut(.cancelAction)
+ Button("Send") {
+ Task { await send() }
+ }
+ .keyboardShortcut(.defaultAction)
+ }
+ }
+ }
+
+ private func sendingView(message: String) -> some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack(spacing: 10) {
+ ProgressView().controlSize(.small)
+ Text(message)
+ .foregroundColor(.secondary)
+ }
+ HStack {
+ Spacer()
+ Button("Cancel") { onDismiss() }
+ .keyboardShortcut(.cancelAction)
+ .disabled(true)
+ Button("Send") {}
+ .disabled(true)
+ }
+ }
+ }
+
+ private func successView(message: String) -> some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack(alignment: .top, spacing: 10) {
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundColor(.green)
+ .font(.title2)
+ Text(message)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ HStack {
+ Button {
+ openGitHubIssue()
+ } label: {
+ HStack(spacing: 4) {
+ Image(systemName: "arrow.up.right.square")
+ Text("Open GitHub Issue")
+ }
+ }
+ Spacer()
+ Button("Done") { onDismiss() }
+ .keyboardShortcut(.defaultAction)
+ }
+ }
+ }
+
+ private func failureView(message: String) -> some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack(alignment: .top, spacing: 10) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .foregroundColor(.orange)
+ .font(.title2)
+ Text(message)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ HStack {
+ Spacer()
+ Button("Try Again") {
+ phase = .prompting
+ }
+ Button("Close") { onDismiss() }
+ .keyboardShortcut(.defaultAction)
+ }
+ }
+ }
+
+ private func send() async {
+ phase = .sending("Collecting logs and uploading…")
+ let service = BugReportService()
+ let description = userDescription.trimmingCharacters(in: .whitespacesAndNewlines)
+ do {
+ let outcome = try await service.sendReport(
+ isManual: true,
+ userDescription: description.isEmpty ? nil : description
+ )
+ if outcome.success {
+ phase = .success(outcome.message)
+ } else {
+ phase = .failure(outcome.message)
+ }
+ } catch {
+ phase = .failure(error.localizedDescription)
+ }
+ }
+
+ private func openGitHubIssue() {
+ let description = userDescription.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ var bodyParts: [String] = []
+ bodyParts.append("## Describe the bug")
+ bodyParts.append("")
+ if !description.isEmpty {
+ bodyParts.append(description)
+ } else {
+ bodyParts.append("A clear and concise description of what the bug is.")
+ }
+ bodyParts.append("")
+ bodyParts.append("## Environment")
+ bodyParts.append("")
+ bodyParts.append("- macOS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)")
+ bodyParts.append("- EXO Version: \(buildTag) (\(buildCommit))")
+ bodyParts.append("")
+ bodyParts.append("## Additional context")
+ bodyParts.append("")
+ bodyParts.append("A bug report with diagnostic logs was submitted via the app.")
+
+ let body = bodyParts.joined(separator: "\n")
+
+ var components = URLComponents(string: "https://github.com/exo-explore/exo/issues/new")!
+ components.queryItems = [
+ URLQueryItem(name: "template", value: "bug_report.md"),
+ URLQueryItem(name: "title", value: "[BUG] "),
+ URLQueryItem(name: "body", value: body),
+ URLQueryItem(name: "labels", value: "bug"),
+ ]
+
+ if let url = components.url {
+ NSWorkspace.shared.open(url)
+ }
+ }
+
+ private var buildTag: String {
+ Bundle.main.infoDictionary?["EXOBuildTag"] as? String ?? "unknown"
+ }
+
+ private var buildCommit: String {
+ Bundle.main.infoDictionary?["EXOBuildCommit"] as? String ?? "unknown"
+ }
+}
diff --git a/app/EXO/EXO/Views/SettingsView.swift b/app/EXO/EXO/Views/SettingsView.swift
index 425cc760..bb1f1bb7 100644
--- a/app/EXO/EXO/Views/SettingsView.swift
+++ b/app/EXO/EXO/Views/SettingsView.swift
@@ -21,8 +21,6 @@ struct SettingsView: View {
@State private var pendingReadOnlyModelsDirs: String = ""
@State private var pendingCustomEnvironmentVariables: [CustomEnvironmentVariable] = []
@State private var needsRestart = false
- @State private var bugReportInFlight = false
- @State private var bugReportMessage: String?
@State private var uninstallInProgress = false
var body: some View {
@@ -202,8 +200,6 @@ struct SettingsView: View {
VStack(alignment: .leading, spacing: 2) {
rdmaStatusView
}
-
- sendBugReportButton
}
Section("Danger Zone") {
@@ -504,50 +500,8 @@ struct SettingsView: View {
}
}
- private var sendBugReportButton: some View {
- VStack(alignment: .leading, spacing: 4) {
- Button {
- Task {
- await sendBugReport()
- }
- } label: {
- HStack {
- if bugReportInFlight {
- ProgressView()
- .scaleEffect(0.6)
- }
- Text("Send Bug Report")
- .font(.caption)
- .fontWeight(.semibold)
- Spacer()
- }
- }
- .disabled(bugReportInFlight)
-
- if let message = bugReportMessage {
- Text(message)
- .font(.caption2)
- .foregroundColor(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- }
-
// MARK: - Actions
- private func sendBugReport() async {
- bugReportInFlight = true
- bugReportMessage = "Collecting logs..."
- let service = BugReportService()
- do {
- let outcome = try await service.sendReport(isManual: true)
- bugReportMessage = outcome.message
- } catch {
- bugReportMessage = error.localizedDescription
- }
- bugReportInFlight = false
- }
-
private func showUninstallConfirmationAlert() {
let alert = NSAlert()
alert.messageText = "Uninstall EXO"
← 667a3bb0 feat: keep-models option when uninstalling EXO (#1997)
·
back to Exo
·
fix(app): tighten Share Bug Report prompt layout (#2008) fb12b403 →