mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-16 13:05:53 +00:00
Better folder selection with git repo discovery (#274)
* added folder selection with git repo discovery * Increase touch target for repository and folder selection buttons --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
43c98cac4b
commit
ac2f3da586
7 changed files with 459 additions and 18 deletions
|
|
@ -15,6 +15,7 @@ enum AppConstants {
|
|||
static let welcomeVersion = "welcomeVersion"
|
||||
static let preventSleepWhenRunning = "preventSleepWhenRunning"
|
||||
static let enableScreencapService = "enableScreencapService"
|
||||
static let repositoryBasePath = "repositoryBasePath"
|
||||
}
|
||||
|
||||
/// Default values for UserDefaults
|
||||
|
|
@ -23,6 +24,8 @@ enum AppConstants {
|
|||
static let preventSleepWhenRunning = true
|
||||
/// Screencap service is enabled by default for screen sharing
|
||||
static let enableScreencapService = true
|
||||
/// Default repository base path for auto-discovery
|
||||
static let repositoryBasePath = "~/"
|
||||
}
|
||||
|
||||
/// Helper to get boolean value with proper default
|
||||
|
|
@ -40,4 +43,24 @@ enum AppConstants {
|
|||
}
|
||||
return UserDefaults.standard.bool(forKey: key)
|
||||
}
|
||||
|
||||
/// Helper to get string value with proper default
|
||||
static func stringValue(for key: String) -> String {
|
||||
// If the key doesn't exist in UserDefaults, return our default
|
||||
if UserDefaults.standard.object(forKey: key) == nil {
|
||||
switch key {
|
||||
case UserDefaultsKeys.repositoryBasePath:
|
||||
// return last used path if it's exists
|
||||
if let value = UserDefaults.standard.value(forKey: "NewSession.workingDirectory") as? String {
|
||||
return value
|
||||
} else {
|
||||
return Defaults.repositoryBasePath
|
||||
}
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return UserDefaults.standard.string(forKey: key) ?? ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
240
mac/VibeTunnel/Core/Services/RepositoryDiscoveryService.swift
Normal file
240
mac/VibeTunnel/Core/Services/RepositoryDiscoveryService.swift
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import Foundation
|
||||
import OSLog
|
||||
import Observation
|
||||
// MARK: - Logger
|
||||
|
||||
extension Logger {
|
||||
fileprivate static let repositoryDiscovery = Logger(
|
||||
subsystem: "sh.vibetunnel.vibetunnel",
|
||||
category: "RepositoryDiscovery"
|
||||
)
|
||||
}
|
||||
|
||||
/// Service for discovering Git repositories in a specified directory
|
||||
///
|
||||
/// Provides functionality to scan a base directory for Git repositories and
|
||||
/// return them in a format suitable for display in the New Session form.
|
||||
/// Includes caching and performance optimizations for large directory trees.
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class RepositoryDiscoveryService {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Published array of discovered repositories
|
||||
public private(set) var repositories: [DiscoveredRepository] = []
|
||||
|
||||
/// Whether discovery is currently in progress
|
||||
public private(set) var isDiscovering = false
|
||||
|
||||
/// Last error encountered during discovery
|
||||
public private(set) var lastError: String?
|
||||
|
||||
/// Cache of discovered repositories by base path
|
||||
private var repositoryCache: [String: [DiscoveredRepository]] = [:]
|
||||
|
||||
/// Maximum depth to search for repositories (prevents infinite recursion)
|
||||
private let maxSearchDepth = 3
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
public init() {}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Discover repositories in the specified base path
|
||||
/// - Parameter basePath: The base directory to search (supports ~ expansion)
|
||||
public func discoverRepositories(in basePath: String) async {
|
||||
guard !isDiscovering else {
|
||||
Logger.repositoryDiscovery.debug("Discovery already in progress, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
isDiscovering = true
|
||||
lastError = nil
|
||||
|
||||
let expandedPath = NSString(string: basePath).expandingTildeInPath
|
||||
Logger.repositoryDiscovery.info("Starting repository discovery in: \(expandedPath)")
|
||||
|
||||
// Check cache first
|
||||
if let cachedRepositories = repositoryCache[expandedPath] {
|
||||
Logger.repositoryDiscovery.debug("Using cached repositories for path: \(expandedPath)")
|
||||
repositories = cachedRepositories
|
||||
isDiscovering = false
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached { [weak self] in
|
||||
// Perform discovery in background
|
||||
let discoveredRepos = await self?.performDiscovery(in: expandedPath)
|
||||
|
||||
guard let discoveredRepos else {
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run { [weak self] in
|
||||
// Cache and update results
|
||||
self?.repositoryCache[expandedPath] = discoveredRepos
|
||||
self?.repositories = discoveredRepos
|
||||
|
||||
Logger.repositoryDiscovery.info("Discovered \(discoveredRepos.count) repositories in: \(expandedPath)")
|
||||
|
||||
self?.isDiscovering = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the repository cache
|
||||
public func clearCache() {
|
||||
repositoryCache.removeAll()
|
||||
Logger.repositoryDiscovery.debug("Repository cache cleared")
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Perform the actual discovery work
|
||||
private nonisolated func performDiscovery(in basePath: String) async -> [DiscoveredRepository] {
|
||||
return await withTaskGroup(of: [DiscoveredRepository].self) { taskGroup in
|
||||
var allRepositories: [DiscoveredRepository] = []
|
||||
|
||||
// Submit discovery task
|
||||
taskGroup.addTask { [weak self] in
|
||||
await self?.scanDirectory(basePath, depth: 0) ?? []
|
||||
}
|
||||
|
||||
// Collect results
|
||||
for await repositories in taskGroup {
|
||||
allRepositories.append(contentsOf: repositories)
|
||||
}
|
||||
|
||||
// Sort by folder name for consistent display
|
||||
return allRepositories.sorted { $0.folderName < $1.folderName }
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively scan a directory for Git repositories
|
||||
private nonisolated func scanDirectory(_ path: String, depth: Int) async -> [DiscoveredRepository] {
|
||||
guard depth < maxSearchDepth else {
|
||||
Logger.repositoryDiscovery.debug("Max depth reached at: \(path)")
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let url = URL(fileURLWithPath: path)
|
||||
|
||||
// Check if directory is accessible
|
||||
guard fileManager.isReadableFile(atPath: path) else {
|
||||
Logger.repositoryDiscovery.debug("Directory not readable: \(path)")
|
||||
return []
|
||||
}
|
||||
|
||||
// Get directory contents
|
||||
let contents = try fileManager.contentsOfDirectory(
|
||||
at: url,
|
||||
includingPropertiesForKeys: [.isDirectoryKey, .isHiddenKey],
|
||||
options: [.skipsSubdirectoryDescendants]
|
||||
)
|
||||
|
||||
var repositories: [DiscoveredRepository] = []
|
||||
|
||||
for itemURL in contents {
|
||||
let resourceValues = try itemURL.resourceValues(forKeys: [.isDirectoryKey, .isHiddenKey])
|
||||
|
||||
// Skip files and hidden directories (except .git)
|
||||
guard resourceValues.isDirectory == true else { continue }
|
||||
if resourceValues.isHidden == true && itemURL.lastPathComponent != ".git" {
|
||||
continue
|
||||
}
|
||||
|
||||
let itemPath = itemURL.path
|
||||
|
||||
// Check if this directory is a Git repository
|
||||
if isGitRepository(at: itemPath) {
|
||||
let repository = await createDiscoveredRepository(at: itemPath)
|
||||
repositories.append(repository)
|
||||
} else {
|
||||
// Recursively scan subdirectories
|
||||
let subdirectoryRepos = await scanDirectory(itemPath, depth: depth + 1)
|
||||
repositories.append(contentsOf: subdirectoryRepos)
|
||||
}
|
||||
}
|
||||
|
||||
return repositories
|
||||
|
||||
} catch {
|
||||
Logger.repositoryDiscovery.error("Error scanning directory \(path): \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a directory is a Git repository
|
||||
private nonisolated func isGitRepository(at path: String) -> Bool {
|
||||
let gitPath = URL(fileURLWithPath: path).appendingPathComponent(".git").path
|
||||
return FileManager.default.fileExists(atPath: gitPath)
|
||||
}
|
||||
|
||||
/// Create a DiscoveredRepository from a path
|
||||
private nonisolated func createDiscoveredRepository(at path: String) async -> DiscoveredRepository {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let folderName = url.lastPathComponent
|
||||
|
||||
// Get last modified date
|
||||
let lastModified = getLastModifiedDate(at: path)
|
||||
|
||||
// Get GitHub URL (this might be slow, so we do it in background)
|
||||
let githubURL = GitRepository.getGitHubURL(for: path)
|
||||
|
||||
return DiscoveredRepository(
|
||||
path: path,
|
||||
folderName: folderName,
|
||||
lastModified: lastModified,
|
||||
githubURL: githubURL
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the last modified date of a repository
|
||||
nonisolated private func getLastModifiedDate(at path: String) -> Date {
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
||||
return attributes[.modificationDate] as? Date ?? Date.distantPast
|
||||
} catch {
|
||||
Logger.repositoryDiscovery.debug("Could not get modification date for \(path): \(error)")
|
||||
return Date.distantPast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DiscoveredRepository
|
||||
|
||||
/// A lightweight repository representation for discovery purposes
|
||||
public struct DiscoveredRepository: Identifiable, Hashable, Sendable {
|
||||
public let id = UUID()
|
||||
public let path: String
|
||||
public let folderName: String
|
||||
public let lastModified: Date
|
||||
public let githubURL: URL?
|
||||
|
||||
/// Display name for the repository
|
||||
public var displayName: String {
|
||||
folderName
|
||||
}
|
||||
|
||||
/// Relative path from home directory if applicable
|
||||
public var relativePath: String {
|
||||
let homeDir = NSHomeDirectory()
|
||||
if path.hasPrefix(homeDir) {
|
||||
return "~" + path.dropFirst(homeDir.count)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/// Formatted last modified date
|
||||
public var formattedLastModified: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .none
|
||||
return formatter.string(from: lastModified)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ struct NewSessionForm: View {
|
|||
private var sessionMonitor
|
||||
@Environment(SessionService.self)
|
||||
private var sessionService
|
||||
@Environment(RepositoryDiscoveryService.self)
|
||||
private var repositoryDiscovery
|
||||
|
||||
// Form fields
|
||||
@State private var command = "zsh"
|
||||
|
|
@ -26,6 +28,7 @@ struct NewSessionForm: View {
|
|||
@State private var showError = false
|
||||
@State private var errorMessage = ""
|
||||
@State private var isHoveringCreate = false
|
||||
@State private var showingRepositoryDropdown = false
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
enum Field: Hashable {
|
||||
|
|
@ -142,18 +145,45 @@ struct NewSessionForm: View {
|
|||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
TextField("~/", text: $workingDirectory)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused($focusedField, equals: .directory)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
TextField("~/", text: $workingDirectory)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused($focusedField, equals: .directory)
|
||||
|
||||
Button(action: selectDirectory) {
|
||||
Image(systemName: "folder")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
Button(action: selectDirectory) {
|
||||
Image(systemName: "folder")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 20, height: 20)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Choose directory")
|
||||
|
||||
Button(action: { showingRepositoryDropdown.toggle() }) {
|
||||
Image(systemName: "arrow.trianglehead.pull")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
.animation(.easeInOut(duration: 0.2), value: showingRepositoryDropdown)
|
||||
.frame(width: 20, height: 20)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Choose from repositories")
|
||||
.disabled(repositoryDiscovery.repositories.isEmpty || repositoryDiscovery.isDiscovering)
|
||||
}
|
||||
|
||||
// Repository dropdown
|
||||
if showingRepositoryDropdown && !repositoryDiscovery.repositories.isEmpty {
|
||||
RepositoryDropdownList(
|
||||
repositories: repositoryDiscovery.repositories,
|
||||
isDiscovering: repositoryDiscovery.isDiscovering,
|
||||
selectedPath: $workingDirectory,
|
||||
isShowing: $showingRepositoryDropdown
|
||||
)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Choose directory")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -319,6 +349,11 @@ struct NewSessionForm: View {
|
|||
.onAppear {
|
||||
loadPreferences()
|
||||
focusedField = .name
|
||||
|
||||
}
|
||||
.task {
|
||||
let repositoryBasePath = AppConstants.stringValue(for: AppConstants.UserDefaultsKeys.repositoryBasePath)
|
||||
await repositoryDiscovery.discoverRepositories(in: repositoryBasePath)
|
||||
}
|
||||
.alert("Error", isPresented: $showError) {
|
||||
Button("OK") {}
|
||||
|
|
@ -442,9 +477,8 @@ struct NewSessionForm: View {
|
|||
if let savedCommand = UserDefaults.standard.string(forKey: "NewSession.command") {
|
||||
command = savedCommand
|
||||
}
|
||||
if let savedDir = UserDefaults.standard.string(forKey: "NewSession.workingDirectory") {
|
||||
workingDirectory = savedDir
|
||||
}
|
||||
|
||||
workingDirectory = AppConstants.stringValue(for: AppConstants.UserDefaultsKeys.repositoryBasePath)
|
||||
|
||||
// Check if spawn window preference has been explicitly set
|
||||
if UserDefaults.standard.object(forKey: "NewSession.spawnWindow") != nil {
|
||||
|
|
@ -468,3 +502,73 @@ struct NewSessionForm: View {
|
|||
UserDefaults.standard.set(titleMode.rawValue, forKey: "NewSession.titleMode")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Repository Dropdown List
|
||||
|
||||
private struct RepositoryDropdownList: View {
|
||||
let repositories: [DiscoveredRepository]
|
||||
let isDiscovering: Bool
|
||||
@Binding var selectedPath: String
|
||||
@Binding var isShowing: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(repositories) { repository in
|
||||
Button(action: {
|
||||
selectedPath = repository.path
|
||||
isShowing = false
|
||||
}) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(repository.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(repository.relativePath)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(repository.formattedLastModified)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.clear)
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hovering in
|
||||
if hovering {
|
||||
// Add hover effect if needed
|
||||
}
|
||||
}
|
||||
|
||||
if repository.id != repositories.last?.id {
|
||||
Divider()
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(.regularMaterial)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ final class StatusBarController: NSObject {
|
|||
private let tailscaleService: TailscaleService
|
||||
private let terminalLauncher: TerminalLauncher
|
||||
private let gitRepositoryMonitor: GitRepositoryMonitor
|
||||
private let repositoryDiscovery: RepositoryDiscoveryService
|
||||
|
||||
// MARK: - State Tracking
|
||||
|
||||
|
|
@ -41,7 +42,8 @@ final class StatusBarController: NSObject {
|
|||
ngrokService: NgrokService,
|
||||
tailscaleService: TailscaleService,
|
||||
terminalLauncher: TerminalLauncher,
|
||||
gitRepositoryMonitor: GitRepositoryMonitor
|
||||
gitRepositoryMonitor: GitRepositoryMonitor,
|
||||
repositoryDiscovery: RepositoryDiscoveryService
|
||||
) {
|
||||
self.sessionMonitor = sessionMonitor
|
||||
self.serverManager = serverManager
|
||||
|
|
@ -49,7 +51,8 @@ final class StatusBarController: NSObject {
|
|||
self.tailscaleService = tailscaleService
|
||||
self.terminalLauncher = terminalLauncher
|
||||
self.gitRepositoryMonitor = gitRepositoryMonitor
|
||||
|
||||
self.repositoryDiscovery = repositoryDiscovery
|
||||
|
||||
self.menuManager = StatusBarMenuManager()
|
||||
|
||||
super.init()
|
||||
|
|
@ -90,7 +93,8 @@ final class StatusBarController: NSObject {
|
|||
ngrokService: ngrokService,
|
||||
tailscaleService: tailscaleService,
|
||||
terminalLauncher: terminalLauncher,
|
||||
gitRepositoryMonitor: gitRepositoryMonitor
|
||||
gitRepositoryMonitor: gitRepositoryMonitor,
|
||||
repositoryDiscovery: repositoryDiscovery
|
||||
)
|
||||
menuManager.setup(with: configuration)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ final class StatusBarMenuManager: NSObject {
|
|||
private var tailscaleService: TailscaleService?
|
||||
private var terminalLauncher: TerminalLauncher?
|
||||
private var gitRepositoryMonitor: GitRepositoryMonitor?
|
||||
private var repositoryDiscovery: RepositoryDiscoveryService?
|
||||
|
||||
// Custom window management
|
||||
fileprivate var customWindow: CustomMenuWindow?
|
||||
|
|
@ -78,6 +79,7 @@ final class StatusBarMenuManager: NSObject {
|
|||
let tailscaleService: TailscaleService
|
||||
let terminalLauncher: TerminalLauncher
|
||||
let gitRepositoryMonitor: GitRepositoryMonitor
|
||||
let repositoryDiscovery: RepositoryDiscoveryService
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
|
@ -89,6 +91,7 @@ final class StatusBarMenuManager: NSObject {
|
|||
self.tailscaleService = configuration.tailscaleService
|
||||
self.terminalLauncher = configuration.terminalLauncher
|
||||
self.gitRepositoryMonitor = configuration.gitRepositoryMonitor
|
||||
self.repositoryDiscovery = configuration.repositoryDiscovery
|
||||
}
|
||||
|
||||
// MARK: - State Management
|
||||
|
|
@ -143,6 +146,7 @@ final class StatusBarMenuManager: NSObject {
|
|||
.environment(terminalLauncher)
|
||||
.environment(sessionService)
|
||||
.environment(gitRepositoryMonitor)
|
||||
.environment(repositoryDiscovery)
|
||||
|
||||
// Wrap in custom container for proper styling
|
||||
let containerView = CustomMenuContainer {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ struct AdvancedSettingsView: View {
|
|||
private var cleanupOnStartup = true
|
||||
@AppStorage("showInDock")
|
||||
private var showInDock = true
|
||||
@AppStorage("repositoryBasePath")
|
||||
private var repositoryBasePath = AppConstants.Defaults.repositoryBasePath
|
||||
@State private var cliInstaller = CLIInstaller()
|
||||
@State private var showingVtConflictAlert = false
|
||||
|
||||
|
|
@ -25,6 +27,9 @@ struct AdvancedSettingsView: View {
|
|||
// Apps preference section
|
||||
TerminalPreferenceSection()
|
||||
|
||||
// Repository section
|
||||
RepositorySettingsSection(repositoryBasePath: $repositoryBasePath)
|
||||
|
||||
// Integration section
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
|
@ -569,3 +574,58 @@ private struct WindowHighlightSettingsSection: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Repository Settings Section
|
||||
|
||||
private struct RepositorySettingsSection: View {
|
||||
@Binding var repositoryBasePath: String
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 8) {
|
||||
TextField("Default base path", text: $repositoryBasePath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
|
||||
Button(action: selectDirectory) {
|
||||
Image(systemName: "folder")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Choose directory")
|
||||
}
|
||||
|
||||
Text("Base path where VibeTunnel will search for Git repositories to show in the New Session form.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Repository Discovery")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text("Git repositories found in this directory will appear in the New Session form for quick access.")
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectDirectory() {
|
||||
let panel = NSOpenPanel()
|
||||
panel.canChooseFiles = false
|
||||
panel.canChooseDirectories = true
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.directoryURL = URL(fileURLWithPath: NSString(string: repositoryBasePath).expandingTildeInPath)
|
||||
|
||||
if panel.runModal() == .OK, let url = panel.url {
|
||||
let path = url.path
|
||||
let homeDir = NSHomeDirectory()
|
||||
if path.hasPrefix(homeDir) {
|
||||
repositoryBasePath = "~" + path.dropFirst(homeDir.count)
|
||||
} else {
|
||||
repositoryBasePath = path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ struct VibeTunnelApp: App {
|
|||
@State var permissionManager = SystemPermissionManager.shared
|
||||
@State var terminalLauncher = TerminalLauncher.shared
|
||||
@State var gitRepositoryMonitor = GitRepositoryMonitor()
|
||||
@State var repositoryDiscoveryService = RepositoryDiscoveryService()
|
||||
@State var screencapService: ScreencapService?
|
||||
|
||||
init() {
|
||||
|
|
@ -47,6 +48,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
.environment(gitRepositoryMonitor)
|
||||
.environment(repositoryDiscoveryService)
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
.defaultSize(width: 580, height: 480)
|
||||
|
|
@ -65,6 +67,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
.environment(gitRepositoryMonitor)
|
||||
.environment(repositoryDiscoveryService)
|
||||
} else {
|
||||
Text("Session not found")
|
||||
.frame(width: 400, height: 300)
|
||||
|
|
@ -83,6 +86,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
.environment(gitRepositoryMonitor)
|
||||
.environment(repositoryDiscoveryService)
|
||||
}
|
||||
.commands {
|
||||
CommandGroup(after: .appInfo) {
|
||||
|
|
@ -273,7 +277,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
|
|||
let ngrokService = app?.ngrokService,
|
||||
let tailscaleService = app?.tailscaleService,
|
||||
let terminalLauncher = app?.terminalLauncher,
|
||||
let gitRepositoryMonitor = app?.gitRepositoryMonitor
|
||||
let gitRepositoryMonitor = app?.gitRepositoryMonitor,
|
||||
let repositoryDiscoveryService = app?.repositoryDiscoveryService
|
||||
{
|
||||
// Connect GitRepositoryMonitor to SessionMonitor for pre-caching
|
||||
sessionMonitor.gitRepositoryMonitor = gitRepositoryMonitor
|
||||
|
|
@ -284,7 +289,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
|
|||
ngrokService: ngrokService,
|
||||
tailscaleService: tailscaleService,
|
||||
terminalLauncher: terminalLauncher,
|
||||
gitRepositoryMonitor: gitRepositoryMonitor
|
||||
gitRepositoryMonitor: gitRepositoryMonitor,
|
||||
repositoryDiscovery: repositoryDiscoveryService
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue