8000 Merge pull request #35 from aaronjeline/fix/ui-delete-performance · symposium-dev/symposium@a78c131 · GitHub
[go: up one dir, main page]

Skip to content

Commit a78c131

Browse files
authored
Merge pull request #35 from aaronjeline/fix/ui-delete-performance
Fix UI lag during taskspace deletion by making operations async
2 parents 33910a9 + 41cb416 commit a78c131

File tree

3 files changed

+88
-30
lines changed

3 files changed

+88
-30
lines changed

.vscode/launch.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"configurations": [
3+
{
4+
"type": "swift",
5+
"request": "launch",
6+
"args": [],
7+
"cwd": "${workspaceFolder:symposium}/symposium/macos-app",
8+
"name": "Debug Symposium (symposium/macos-app)",
9+
"program": "${workspaceFolder:symposium}/symposium/macos-app/.build/debug/Symposium",
10+
"preLaunchTask": "swift: Build Debug Symposium (symposium/macos-app)"
11+
},
12+
{
13+
"type": "swift",
14+
"request": "launch",
15+
"args": [],
16+
"cwd": "${workspaceFolder:symposium}/symposium/macos-app",
17+
"name": "Release Symposium (symposium/macos-app)",
18+
"program": "${workspaceFolder:symposium}/symposium/macos-app/.build/release/Symposium",
19+
"preLaunchTask": "swift: Build Release Symposium (symposium/macos-app)"
20+
}
21+
]
22+
}

symposium/macos-app/Sources/Symposium/Models/ProjectManager.swift

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,27 @@ class ProjectManager: ObservableObject, IpcMessageDelegate {
140140
return (process.terminationStatus, stdout, stderr)
141141
}
142142

143+
private func executeProcessAsync(
144+
executable: String,
145+
arguments: [String],
146+
workingDirectory: String? = nil
147+
) async throws -> (exitCode: Int32, stdout: String, stderr: String) {
148+
return try await withCheckedThrowingContinuation { continuation in
149+
Task.detached {
150+
do {
151+
let result = try self.executeProcess(
152+
executable: executable,
153+
arguments: arguments,
154+
workingDirectory: workingDirectory
155+
)
156+
continuation.resume(returning: result)
157+
} catch {
158+
continuation.resume(throwing: error)
159+
}
160+
}
161+
}
162+
}
163+
143164
/// Open an existing Symposium project
144165
func openProject(at directoryPath: String) throws {
145166
isLoading = true
@@ -402,13 +423,13 @@ class ProjectManager: ObservableObject, IpcMessageDelegate {
402423
/// - taskspaceDir = /path/task-UUID (taskspace directory)
403424
/// - worktreeDir = /path/task-UUID/reponame (actual git worktree)
404425
/// - Git commands must target worktreeDir and run from project.directoryPath (bare repo)
405-
func deleteTaskspace(_ taskspace: Taskspace, deleteBranch: Bool = false) throws {
426+
func deleteTaskspace(_ taskspace: Taskspace, deleteBranch: Bool = false) async throws {
406427
guard let project = currentProject else {
407428
throw ProjectError.noCurrentProject
408429
}
409430

410-
isLoading = true
411-
defer { isLoading = fa F440 lse }
431+
await MainActor.run { isLoading = true }
432+
defer { Task { @MainActor in isLoading = false } }
412433

413434
let taskspaceDir = taskspace.directoryPath(in: project.directoryPath)
414435
let repoName = extractRepoName(from: project.gitURL)
@@ -422,7 +443,7 @@ class ProjectManager: ObservableObject, IpcMessageDelegate {
422443
Logger.shared.log("Attempting to remove worktree: \(worktreeDir) from directory: \(project.directoryPath)")
423444

424445
do {
425-
let result = try executeProcess(
446+
let result = try await executeProcessAsync(
426447
executable: "/usr/bin/git",
427448
arguments: ["worktree", "remove", worktreeDir, "--force"],
428449
workingDirectory: project.directoryPath
@@ -444,7 +465,7 @@ class ProjectManager: ObservableObject, IpcMessageDelegate {
444465
// Optionally delete the branch
445466
if deleteBranch && !branchName.isEmpty {
446467
do {
447-
let result = try executeProcess(
468+
let result = try await executeProcessAsync(
448469
executable: "/usr/bin/git",
449470
arguments: ["branch", "-D", branchName],
450471
workingDirectory: project.directoryPath
@@ -459,7 +480,7 @@ class ProjectManager: ObservableObject, IpcMessageDelegate {
459480
}
460481

461482
// Remove from current project
462-
DispatchQueue.main.async {
483+
await MainActor.run {
463484
var updatedProject = project
464485
updatedProject.taskspaces.removeAll { $0.id == taskspace.id }
465486
self.currentProject = updatedProject

symposium/macos-app/Sources/Symposium/Views/ProjectView.swift

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ struct TaskspaceCard: View {
386386
@ObservedObject var projectManager: ProjectManager
387387
@State private var showingDeleteConfirmation = false
388388
@State private var deleteBranch = false
389+
@State private var cachedBranchInfo: (branchName: String, isMerged: Bool, unmergedCommits: Int, hasUncommittedChanges: Bool) = ("", false, 0, false)
389390
@State private var isHovered = false
390391
@State private var isPressed = false
391392

@@ -632,12 +633,16 @@ struct TaskspaceCard: View {
632633
projectManager: projectManager,
633634
deleteBranch: $deleteBranch,
634635
onConfirm: {
635-
do {
636-
try projectManager.deleteTaskspace(taskspace, deleteBranch: deleteBranch)
637-
} catch {
638-
Logger.shared.log("Failed to delete taskspace: \(error)")
636+
Task {
637+
do {
638+
try await projectManager.deleteTaskspace(taskspace, deleteBranch: deleteBranch)
639+
} catch {
640+
Logger.shared.log("Failed to delete taskspace: \(error)")
641+
}
642+
await MainActor.run {
643+
showingDeleteConfirmation = false
644+
}
639645
}
640-
showingDeleteConfirmation = false
641646
},
642647
onCancel: {
643648
// Send cancellation response for pending deletion request
@@ -672,14 +677,8 @@ struct DeleteTaskspaceDialog: View {
672677
let onConfirm: () -> Void
673678
let onCancel: () -> Void
674679

675-
/// Computed property that gets fresh branch info when dialog renders
676-
///
677-
/// CRITICAL: This computes fresh data every time the dialog appears, not cached data.
678-
/// Users may make commits between app startup and deletion, so stale info could
679-
/// show incorrect warnings leading to accidental data loss.
680-
private var branchInfo: (branchName: String, isMerged: Bool, unmergedCommits: Int, hasUncommittedChanges: Bool) {
681-
projectManager.getTaskspaceBranchInfo(for: taskspace)
682-
}
680+
@State private var cachedBranchInfo: (branchName: String, isMerged: Bool, unmergedCommits: Int, hasUncommittedChanges: Bool) = ("", false, 0, false)
681+
@State private var isLoadingBranchInfo = true
683682

684683
var body: some View {
685684
VStack(spacing: 20) {
@@ -689,27 +688,35 @@ struct DeleteTaskspaceDialog: View {
689688
Text("Are you sure you want to delete '\(taskspaceName)'? This will permanently remove all files and cannot be undone.")
690689
.multilineTextAlignment(.center)
691690

692-
if !branchInfo.branchName.isEmpty {
691+
if isLoadingBranchInfo {
692+
HStack {
693+
ProgressView()
694+
.scaleEffect(0.8)
695+
Text("Checking branch status...")
696+
.font(.caption)
697+
.foregroundColor(.secondary)
698+
}
699+
} else if !cachedBranchInfo.branchName.isEmpty {
693700
VStack(alignment: .leading, spacing: 8) {
694701
HStack {
695-
Toggle("Also delete the branch `\(branchInfo.branchName)` from git", isOn: $deleteBranch)
702+
Toggle("Also delete the branch `\(cachedBranchInfo.branchName)` from git", isOn: $deleteBranch)
696703
Spacer()
697704
}
698705

699-
if branchInfo.unmergedCommits > 0 || branchInfo.hasUncommittedChanges {
706+
if cachedBranchInfo.unmergedCommits > 0 || cachedBranchInfo.hasUncommittedChanges {
700707
VStack(alignment: .leading, spacing: 4) {
701-
if branchInfo.unmergedCommits > 0 {
708+
if cachedBranchInfo.unmergedCommits > 0 {
702709
HStack {
703710
Image(systemName: "exclamationmark.triangle.fill")
704711
.foregroundColor(.orange)
705-
Text("\(branchInfo.unmergedCommits) commit\(branchInfo.unmergedCommits == 1 ? "" : "s") from this branch do not appear in the main branch.")
712+
Text("\(cachedBranchInfo.unmergedCommits) commit\(cachedBranchInfo.unmergedCommits == 1 ? "" : "s") from this branch do not appear in the main branch.")
706713
.font(.caption)
707714
.foregroundColor(.orange)
708715
}
709716
.padding(.leading, 20)
710717
}
711718

712-
if branchInfo.hasUncommittedChanges {
719+
if cachedBranchInfo.hasUncommittedChanges {
713720
HStack {
714721
Image(systemName: "exclamationmark.triangle.fill")
715722
.foregroundColor(.orange)
@@ -720,7 +727,7 @@ struct DeleteTaskspaceDialog: View {
720727
.padding(.leading, 20)
721728
}
722729

723-
if branchInfo.unmergedCommits > 0 || branchInfo.hasUncommittedChanges {
730+
if cachedBranchInfo.unmergedCommits > 0 || cachedBranchInfo.hasUncommittedChanges {
724731
HStack {
725732
Image(systemName: "exclamationmark.triangle.fill")
726733
.foregroundColor(.orange)
@@ -761,10 +768,18 @@ struct DeleteTaskspaceDialog: View {
761768
}
762769
}
763770
.onAppear {
764-
// Set default deleteBranch toggle based on safety analysis
765-
// Safe branches (no unmerged commits, no uncommitted changes): checked by default (encourage cleanup)
766-
// Risky branches: unchecked by default (prevent accidental loss)
767-
deleteBranch = (branchInfo.unmergedCommits == 0 && !branchInfo.hasUncommittedChanges)
771+
Task {
772+
let manager = projectManager
773+
let ts = taskspace
774+
cachedBranchInfo = await Task.detached {
775+
manager.getTaskspaceBranchInfo(for: ts)
776+
}.value
777+
778+
isLoadingBranchInfo = false
779+
780+
// Set default deleteBranch toggle based on safety analysis
781+
deleteBranch = (cachedBranchInfo.unmergedCommits == 0 && !cachedBranchInfo.hasUncommittedChanges)
782+
}
768783
}
769784
.padding()
770785
.frame(width: 400)

0 commit comments

Comments
 (0)
0