native macOS codings agent orchestrator
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add repo settings and lint tweaks

khoi fb4e882c 3afd5ef9

+185 -28
+1 -1
.swift-format.json
··· 12 12 "lineBreakBeforeEachArgument" : false, 13 13 "lineBreakBeforeEachGenericRequirement" : false, 14 14 "lineBreakBetweenDeclarationAttributes" : false, 15 - "lineLength" : 100, 15 + "lineLength" : 120, 16 16 "maximumBlankLines" : 1, 17 17 "multiElementCollectionTrailingCommas" : true, 18 18 "noAssignmentInExpressions" : {
+5 -8
.swiftlint.yml
··· 4 4 excluded: 5 5 - ThirdParty/ghostty 6 6 7 + disabled_rules: 8 + - file_length 9 + - closure_parameter_position 10 + - type_body_length 11 + 7 12 opt_in_rules: 8 13 - accessibility_label_for_image 9 14 - accessibility_trait_for_button ··· 21 26 warning: 15 22 27 error: 20 23 28 24 - file_length: 25 - warning: 600 26 - error: 800 27 - 28 29 function_body_length: 29 30 warning: 100 30 31 error: 150 31 - 32 - type_body_length: 33 - warning: 500 34 - error: 650 35 32 36 33 redundant_discardable_let: 37 34 ignore_swiftui_view_bodies: true
+2 -1
supacode/Commands/UpdateCommands.swift
··· 10 10 updateController.checkForUpdates() 11 11 } 12 12 .keyboardShortcut( 13 - AppShortcuts.checkForUpdates.keyEquivalent, modifiers: AppShortcuts.checkForUpdates.modifiers 13 + AppShortcuts.checkForUpdates.keyEquivalent, 14 + modifiers: AppShortcuts.checkForUpdates.modifiers 14 15 ) 15 16 .help("Check for Updates (\(AppShortcuts.checkForUpdates.display))") 16 17 }
+22 -1
supacode/ContentView.swift
··· 362 362 let onRequestRemoval: (Worktree, Repository) -> Void 363 363 let onRequestRepositoryRemoval: (Repository) -> Void 364 364 @Environment(RepositoryStore.self) private var repositoryStore 365 + @Environment(\.openWindow) private var openWindow 365 366 366 367 var body: some View { 367 368 let isExpanded = expandedRepoIDs.contains(repository.id) 368 369 let isRemovingRepository = repositoryStore.isRemovingRepository(repository) 370 + let openRepoSettings = { 371 + openWindow(id: WindowIdentifiers.repoSettings, value: repository.id) 372 + } 369 373 Section { 370 374 WorktreeRowsView( 371 375 repository: repository, ··· 394 398 .buttonStyle(.plain) 395 399 .disabled(isRemovingRepository) 396 400 .contextMenu { 401 + Button("Repo Settings") { 402 + openRepoSettings() 403 + } 404 + .help("Repo Settings (no shortcut)") 397 405 Button("Remove Repository") { 398 406 onRequestRepositoryRemoval(repository) 399 407 } ··· 405 413 ProgressView() 406 414 .controlSize(.small) 407 415 } 416 + Menu { 417 + Button("Repo Settings") { 418 + openRepoSettings() 419 + } 420 + .help("Repo Settings (no shortcut)") 421 + } label: { 422 + Label("Repository options", systemImage: "ellipsis") 423 + } 424 + .labelStyle(.iconOnly) 425 + .buttonStyle(.plain) 426 + .foregroundStyle(.primary) 427 + .help("Repository options (no shortcut)") 428 + .disabled(isRemovingRepository) 408 429 Button("New Worktree", systemImage: "plus") { 409 430 createWorktree(repository) 410 431 } ··· 440 461 private func rowView(_ row: WorktreeRowModel, isRepositoryRemoving: Bool) -> some View { 441 462 let displayDetail = row.isDeleting ? "Removing..." : row.detail 442 463 if row.isRemovable, let worktree = repositoryStore.worktree(for: row.id), 443 - !isRepositoryRemoving 464 + !isRepositoryRemoving 444 465 { 445 466 WorktreeRow( 446 467 name: row.name,
+6 -2
supacode/GhosttyEmbed/GhosttyRuntime.swift
··· 99 99 return Unmanaged<GhosttyRuntime>.fromOpaque(userdata).takeUnretainedValue() 100 100 } 101 101 102 - private static func surfaceBridge(fromUserdata userdata: UnsafeMutableRawPointer?) -> GhosttySurfaceBridge? { 102 + private static func surfaceBridge(fromUserdata userdata: UnsafeMutableRawPointer?) 103 + -> GhosttySurfaceBridge? 104 + { 103 105 guard let userdata else { return nil } 104 106 return Unmanaged<GhosttySurfaceBridge>.fromOpaque(userdata).takeUnretainedValue() 105 107 } 106 108 107 - private static func surfaceBridge(fromSurface surface: ghostty_surface_t?) -> GhosttySurfaceBridge? { 109 + private static func surfaceBridge(fromSurface surface: ghostty_surface_t?) 110 + -> GhosttySurfaceBridge? 111 + { 108 112 guard let surface, let userdata = ghostty_surface_userdata(surface) else { return nil } 109 113 return Unmanaged<GhosttySurfaceBridge>.fromOpaque(userdata).takeUnretainedValue() 110 114 }
+2 -1
supacode/GhosttyEmbed/GhosttySurfaceBridge.swift
··· 200 200 state.keyTableTag = table.tag 201 201 switch table.tag { 202 202 case GHOSTTY_KEY_TABLE_ACTIVATE: 203 - state.keyTableName = string(from: table.value.activate.name, length: table.value.activate.len) 203 + state.keyTableName = string( 204 + from: table.value.activate.name, length: table.value.activate.len) 204 205 default: 205 206 state.keyTableName = nil 206 207 }
+12 -7
supacode/GitClient.swift
··· 34 34 35 35 nonisolated func worktrees(for repoRoot: URL) async throws -> [Worktree] { 36 36 let baseDirectory = wtBaseDirectory(for: repoRoot) 37 + let repositoryRootURL = repoRoot.standardizedFileURL 37 38 let output = try await runWtList(repoRoot: repoRoot, baseDirectory: baseDirectory) 38 39 let trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines) 39 40 if trimmed.isEmpty { ··· 55 56 id: id, 56 57 name: entry.branch, 57 58 detail: detail, 58 - workingDirectory: worktreeURL 59 + workingDirectory: worktreeURL, 60 + repositoryRootURL: repositoryRootURL 59 61 ), 60 62 createdAt: createdAt, 61 63 index: index ··· 91 93 92 94 nonisolated func createWorktree(named name: String, in repoRoot: URL) async throws -> Worktree { 93 95 let baseDirectory = wtBaseDirectory(for: repoRoot) 96 + let repositoryRootURL = repoRoot.standardizedFileURL 94 97 let wtURL = try wtScriptURL() 95 98 let output = try await runProcess( 96 99 executableURL: wtURL, ··· 106 109 let worktreeURL = URL(fileURLWithPath: pathLine).standardizedFileURL 107 110 let detail = Self.relativePath(from: baseDirectory, to: worktreeURL) 108 111 let id = worktreeURL.path(percentEncoded: false) 109 - return Worktree(id: id, name: name, detail: detail, workingDirectory: worktreeURL) 112 + return Worktree( 113 + id: id, 114 + name: name, 115 + detail: detail, 116 + workingDirectory: worktreeURL, 117 + repositoryRootURL: repositoryRootURL 118 + ) 110 119 } 111 120 112 121 nonisolated func isWorktreeDirty(at worktreeURL: URL) async throws -> Bool { ··· 184 193 } 185 194 186 195 nonisolated private func wtBaseDirectory(for repoRoot: URL) -> URL { 187 - let repoName = repoRoot.lastPathComponent 188 - let fallback = repoRoot.path(percentEncoded: false).replacingOccurrences(of: "/", with: "_") 189 - let name = repoName.isEmpty ? fallback : repoName 190 - return SupacodePaths.reposDirectory 191 - .appending(path: name, directoryHint: .isDirectory) 196 + SupacodePaths.repositoryDirectory(for: repoRoot) 192 197 } 193 198 194 199 nonisolated private func runProcess(
+3 -1
supacode/RepositoryStore.swift
··· 498 498 let repositoryIDs = Set(loaded.map(\.id)) 499 499 pendingWorktrees = pendingWorktrees.filter { repositoryIDs.contains($0.repositoryID) } 500 500 if !isSelectionValid(selectedWorktreeID) { 501 - print("[RepositoryStore] selectedWorktreeID \(String(describing: selectedWorktreeID)) is invalid, clearing") 501 + print( 502 + "[RepositoryStore] selectedWorktreeID \(String(describing: selectedWorktreeID)) is invalid, clearing" 503 + ) 502 504 selectedWorktreeID = nil 503 505 } 504 506 print("[RepositoryStore] applyRepositories done, repositories.count = \(repositories.count)")
+7
supacode/Settings/RepositorySettings.swift
··· 1 + import Foundation 2 + 3 + nonisolated struct RepositorySettings: Codable, Equatable { 4 + var startupCommand: String 5 + 6 + static let `default` = RepositorySettings(startupCommand: "echo 123") 7 + }
+26
supacode/Settings/RepositorySettingsModel.swift
··· 1 + import Foundation 2 + import Observation 3 + 4 + @MainActor 5 + @Observable 6 + final class RepositorySettingsModel { 7 + private let store: RepositorySettingsStore 8 + private let rootURL: URL 9 + private var settings: RepositorySettings 10 + 11 + var startupCommand: String { 12 + get { 13 + settings.startupCommand 14 + } 15 + set { 16 + settings.startupCommand = newValue 17 + store.save(settings, for: rootURL) 18 + } 19 + } 20 + 21 + init(rootURL: URL, store: RepositorySettingsStore = RepositorySettingsStore()) { 22 + self.rootURL = rootURL 23 + self.store = store 24 + settings = store.load(for: rootURL) 25 + } 26 + }
+33
supacode/Settings/RepositorySettingsStore.swift
··· 1 + import Foundation 2 + 3 + nonisolated struct RepositorySettingsStore { 4 + func load(for rootURL: URL) -> RepositorySettings { 5 + let settingsURL = settingsURL(for: rootURL) 6 + if let data = try? Data(contentsOf: settingsURL), 7 + let settings = try? JSONDecoder().decode(RepositorySettings.self, from: data) 8 + { 9 + return settings 10 + } 11 + let defaults = RepositorySettings.default 12 + save(defaults, for: rootURL) 13 + return defaults 14 + } 15 + 16 + func save(_ settings: RepositorySettings, for rootURL: URL) { 17 + do { 18 + let settingsURL = settingsURL(for: rootURL) 19 + let directory = settingsURL.deletingLastPathComponent() 20 + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) 21 + let encoder = JSONEncoder() 22 + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 23 + let data = try encoder.encode(settings) 24 + try data.write(to: settingsURL, options: [.atomic]) 25 + } catch { 26 + } 27 + } 28 + 29 + private func settingsURL(for rootURL: URL) -> URL { 30 + SupacodePaths.repositoryDirectory(for: rootURL) 31 + .appending(path: "settings.json", directoryHint: .notDirectory) 32 + } 33 + }
+24
supacode/Settings/RepositorySettingsView.swift
··· 1 + import SwiftUI 2 + 3 + struct RepositorySettingsView: View { 4 + let repositoryRootURL: URL 5 + @State private var model: RepositorySettingsModel 6 + 7 + init(repositoryRootURL: URL) { 8 + self.repositoryRootURL = repositoryRootURL 9 + _model = State(initialValue: RepositorySettingsModel(rootURL: repositoryRootURL)) 10 + } 11 + 12 + var body: some View { 13 + @Bindable var model = model 14 + 15 + Form { 16 + Section { 17 + TextField("Start up command", text: $model.startupCommand, prompt: Text("echo 123")) 18 + } 19 + } 20 + .formStyle(.grouped) 21 + .scenePadding() 22 + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 23 + } 24 + }
+3 -1
supacode/Settings/UpdatesSettingsView.swift
··· 9 9 VStack(alignment: .leading, spacing: 0) { 10 10 Form { 11 11 Section("Automatic Updates") { 12 - Toggle("Check for updates automatically", isOn: $updateController.automaticallyChecksForUpdates) 12 + Toggle( 13 + "Check for updates automatically", isOn: $updateController.automaticallyChecksForUpdates 14 + ) 13 15 Toggle( 14 16 "Download and install updates automatically", 15 17 isOn: $updateController.automaticallyDownloadsUpdates
+7
supacode/SupacodePaths.swift
··· 10 10 baseDirectory.appending(path: "repos", directoryHint: .isDirectory) 11 11 } 12 12 13 + static func repositoryDirectory(for rootURL: URL) -> URL { 14 + let repoName = rootURL.lastPathComponent 15 + let fallback = rootURL.path(percentEncoded: false).replacing("/", with: "_") 16 + let name = repoName.isEmpty ? fallback : repoName 17 + return reposDirectory.appending(path: name, directoryHint: .isDirectory) 18 + } 19 + 13 20 static var settingsURL: URL { 14 21 baseDirectory.appending(path: "settings.json", directoryHint: .notDirectory) 15 22 }
+17 -5
supacode/Terminals/WorktreeTerminalState.swift
··· 6 6 let controller: BonsplitController 7 7 private let runtime: GhosttyRuntime 8 8 private let worktree: Worktree 9 + private let settingsStore = RepositorySettingsStore() 9 10 private var surfaces: [TabID: GhosttySurfaceView] = [:] 10 11 11 12 init(runtime: GhosttyRuntime, worktree: Worktree) { ··· 52 53 return nil 53 54 } 54 55 controller.selectTab(tabId) 55 - surfaceView(for: tabId, initialInput: "echo \(title)\n").requestFocus() 56 + surfaceView(for: tabId, initialInput: startupInput()).requestFocus() 56 57 return tabId 57 58 } 58 59 ··· 74 75 if let existing = surfaces[tabId] { 75 76 return existing 76 77 } 77 - let resolvedInput = initialInput ?? defaultInitialInput(for: tabId) 78 + let resolvedInput = initialInput ?? defaultInitialInput() 78 79 let view = GhosttySurfaceView( 79 80 runtime: runtime, 80 81 workingDirectory: worktree.workingDirectory, ··· 87 88 return view 88 89 } 89 90 90 - private func defaultInitialInput(for tabId: TabID) -> String? { 91 - guard let title = controller.tab(tabId)?.title else { return nil } 92 - return "echo \(title)\n" 91 + private func defaultInitialInput() -> String? { 92 + startupInput() 93 + } 94 + 95 + private func startupInput() -> String? { 96 + let settings = settingsStore.load(for: worktree.repositoryRootURL) 97 + let command = settings.startupCommand 98 + if command.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { 99 + return nil 100 + } 101 + if command.hasSuffix("\n") { 102 + return command 103 + } 104 + return "\(command)\n" 93 105 } 94 106 95 107 func closeAllSurfaces() {
+3
supacode/WindowIdentifiers.swift
··· 1 + enum WindowIdentifiers { 2 + static let repoSettings = "repo-settings" 3 + }
+1
supacode/Worktree.swift
··· 5 5 let name: String 6 6 let detail: String 7 7 let workingDirectory: URL 8 + let repositoryRootURL: URL 8 9 }
+11
supacode/supacodeApp.swift
··· 5 5 // Created by khoi on 20/1/26. 6 6 // 7 7 8 + import Foundation 8 9 import GhosttyKit 9 10 import SwiftUI 10 11 ··· 51 52 .environment(settings) 52 53 .environment(updateController) 53 54 .preferredColorScheme(settings.preferredColorScheme) 55 + } 56 + .environment(repositoryStore) 57 + WindowGroup("Repo Settings", id: WindowIdentifiers.repoSettings, for: Repository.ID.self) { $repositoryID in 58 + if let repositoryID { 59 + RepositorySettingsView(repositoryRootURL: URL(fileURLWithPath: repositoryID)) 60 + } else { 61 + Text("Select a repository to edit settings.") 62 + .frame(maxWidth: .infinity, maxHeight: .infinity) 63 + .scenePadding() 64 + } 54 65 } 55 66 .environment(repositoryStore) 56 67 .commands {