Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion SupacodeSettingsFeature/Reducer/SettingsFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public struct SettingsFeature {
public var remoteSessionPersistenceEnabled: Bool
public var appVisibility: AppVisibility
public var terminalHibernationEnabled: Bool
public var automaticRepositoryRefreshEnabled: Bool
public var cliInstallState = CLIInstallState.checking
/// Installed editors in menu order, resolved once off the picker's body.
public var installedOpenActions: [OpenWorktreeAction]
Expand Down Expand Up @@ -162,6 +163,7 @@ public struct SettingsFeature {
remoteSessionPersistenceEnabled = settings.remoteSessionPersistenceEnabled
appVisibility = settings.appVisibility
terminalHibernationEnabled = settings.terminalHibernationEnabled
automaticRepositoryRefreshEnabled = settings.automaticRepositoryRefreshEnabled
defaultWorktreeBaseDirectoryPath =
SupacodePaths.normalizedWorktreeBaseDirectoryPath(settings.defaultWorktreeBaseDirectoryPath) ?? ""
}
Expand Down Expand Up @@ -204,7 +206,8 @@ public struct SettingsFeature {
terminateSessionsOnQuit: terminateSessionsOnQuit,
remoteSessionPersistenceEnabled: remoteSessionPersistenceEnabled,
appVisibility: appVisibility,
terminalHibernationEnabled: terminalHibernationEnabled
terminalHibernationEnabled: terminalHibernationEnabled,
automaticRepositoryRefreshEnabled: automaticRepositoryRefreshEnabled
)
}
}
Expand Down Expand Up @@ -358,6 +361,7 @@ public struct SettingsFeature {
state.remoteSessionPersistenceEnabled = normalizedSettings.remoteSessionPersistenceEnabled
state.appVisibility = normalizedSettings.appVisibility
state.terminalHibernationEnabled = normalizedSettings.terminalHibernationEnabled
state.automaticRepositoryRefreshEnabled = normalizedSettings.automaticRepositoryRefreshEnabled
state.defaultWorktreeBaseDirectoryPath = normalizedSettings.defaultWorktreeBaseDirectoryPath ?? ""
state.syncGlobalDefaults(from: normalizedSettings)
synchronizeRepositorySelection(for: &state)
Expand Down
7 changes: 7 additions & 0 deletions SupacodeSettingsFeature/Views/WorktreeSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public struct WorktreeSettingsView: View {
Text("Copies untracked files from the main worktree.")
}
}
Section {
Toggle(isOn: $store.automaticRepositoryRefreshEnabled) {
Text("Refresh repository status in the background")
Text("Keeps changed-line counts, branches, and pull-request status up to date.")
Text("Turn off if it triggers SSH passphrase prompts or GitHub rate limiting.")
}
}
Section("Clean-up") {
Picker(
"Auto-delete archived worktrees",
Expand Down
11 changes: 10 additions & 1 deletion SupacodeSettingsShared/Models/GlobalSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable {
/// Beta: hidden terminal tabs release their renderer after a few minutes of
/// inactivity and reconnect when viewed. On by default.
public var terminalHibernationEnabled: Bool
/// Gates all background repository polling (remote SSH, PR checks, reconcile).
/// On by default; disable to stop SSH passphrase prompts or GitHub rate limiting.
public var automaticRepositoryRefreshEnabled: Bool

public static let `default` = GlobalSettings(
appearanceMode: .dark,
Expand Down Expand Up @@ -177,7 +180,8 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable {
terminateSessionsOnQuit: Bool = false,
remoteSessionPersistenceEnabled: Bool = true,
appVisibility: AppVisibility = .dockAndMenuBar,
terminalHibernationEnabled: Bool = true
terminalHibernationEnabled: Bool = true,
automaticRepositoryRefreshEnabled: Bool = true
) {
self.appearanceMode = appearanceMode
self.defaultEditorID = defaultEditorID
Expand Down Expand Up @@ -214,6 +218,7 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable {
self.remoteSessionPersistenceEnabled = remoteSessionPersistenceEnabled
self.appVisibility = appVisibility
self.terminalHibernationEnabled = terminalHibernationEnabled
self.automaticRepositoryRefreshEnabled = automaticRepositoryRefreshEnabled
}

/// Keys for reading renamed settings fields that no longer
Expand Down Expand Up @@ -388,5 +393,9 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable {
terminalHibernationEnabled =
try container.decodeIfPresent(Bool.self, forKey: .terminalHibernationEnabled)
?? Self.default.terminalHibernationEnabled
// Pre-feature files omit this key; background refresh defaults on.
automaticRepositoryRefreshEnabled =
try container.decodeIfPresent(Bool.self, forKey: .automaticRepositoryRefreshEnabled)
?? Self.default.automaticRepositoryRefreshEnabled
}
}
6 changes: 5 additions & 1 deletion supacode/App/supacodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,11 @@ struct SupacodeApp: App {
_ghosttyShortcuts = State(initialValue: shortcuts)
let terminalManager = Self.makeTerminalManager(runtime: runtime)
_terminalManager = State(initialValue: terminalManager)
let worktreeInfoWatcher = WorktreeInfoWatcherManager()
// Seed the flag at construction so a user who disabled background refresh
// never eats a launch-time SSH / gh burst before the setting is applied.
let worktreeInfoWatcher = WorktreeInfoWatcherManager(
automaticRefreshEnabled: initialSettings.automaticRepositoryRefreshEnabled
)
_worktreeInfoWatcher = State(initialValue: worktreeInfoWatcher)
let keyObserver = CommandKeyObserver()
_commandKeyObserver = State(initialValue: keyObserver)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,28 @@ struct WorktreeInfoWatcherClient {
case setWorktrees([Worktree])
case setSelectedWorktreeID(Worktree.ID?)
case setPullRequestTrackingEnabled(Bool)
case setAutomaticRefreshEnabled(Bool)
case setActive(Bool)
case refresh
case stop
}

/// Distinguishes a background/automatic refresh (suppressed when the user
/// disables automatic repository refresh) from a user-initiated one (always
/// honored).
enum RefreshTrigger: Equatable {
case automatic
case manual
}

enum Event: Equatable {
case branchChanged(worktreeID: Worktree.ID)
case filesChanged(worktreeID: Worktree.ID)
case repositoryPullRequestRefresh(repositoryRootURL: URL, worktreeIDs: [Worktree.ID])
case repositoryPullRequestRefresh(
repositoryRootURL: URL,
worktreeIDs: [Worktree.ID],
trigger: RefreshTrigger
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion supacode/Commands/WorktreeCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private struct WorktreeMainMenu: Commands {
.disabled(snapshot.selectedPullRequestURL == nil || !snapshot.githubIntegrationEnabled)
Divider()
Button("Refresh Worktrees", systemImage: "arrow.clockwise") {
store.send(.repositories(.refreshWorktrees))
store.send(.refreshWorktreesRequested)
}
.appKeyboardShortcut(refresh)
.help("Refresh (\(refresh?.display ?? "none"))")
Expand Down
1 change: 1 addition & 0 deletions supacode/Features/App/Models/WorktreeMenuSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ extension AppFeature.Action {
case .applicationDidBecomeActive, .applicationDidResignActive,
.appLaunched, .scenePhaseChanged, .openActionSelectionChanged,
.refreshInstalledOpenActions, .installedOpenActionsResolved,
.refreshWorktreesRequested,
.worktreeSettingsLoaded, .openSelectedWorktree, .revealInFinder,
.openWorktree, .openWorktreeFailed, .openFile, .requestQuit,
.requestTerminateAllTerminalSessions, .newTerminal, .renameSelectedTerminalTab,
Expand Down
33 changes: 30 additions & 3 deletions supacode/Features/App/Reducer/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
case appLaunched
case scenePhaseChanged(ScenePhase)
case repositories(RepositoriesFeature.Action)
case refreshWorktreesRequested
case settings(SettingsFeature.Action)
case updates(UpdatesFeature.Action)
case commandPalette(CommandPaletteFeature.Action)
Expand Down Expand Up @@ -428,6 +429,14 @@
case .agentPresence:
return .none

case .refreshWorktreesRequested:
return .merge(
.send(.repositories(.refreshWorktrees)),
.run { _ in
await worktreeInfoWatcher.send(.refresh)
}
)

case .scenePhaseChanged(let phase):
switch phase {
case .active:
Expand All @@ -440,10 +449,16 @@
// card reflects external installs (e.g. `claude install`)
// for users who keep the app open across days.
.send(.settings(.refreshAgentIntegrationStates)),
// Resume background git polling only while foreground-active so an
// idle / backgrounded app stays quiet.
.run { _ in await worktreeInfoWatcher.send(.setActive(true)) },
.run { send in
while !Task.isCancelled {
try? await ContinuousClock().sleep(for: .seconds(30))
guard !Task.isCancelled else { return }
// Worktree discovery stays ungated so externally created /
// removed worktrees still sync; the setting gates status
// polling (line counts, branch, PR, remote SSH) in the watcher.
await send(.repositories(.refreshWorktrees))
await send(.refreshInstalledOpenActions)
}
Expand All @@ -457,6 +472,7 @@
let agentsBySurface = state.agentPresence.agentsBySurface()
return .merge(
.cancel(id: CancelID.periodicRefresh),
.run { _ in await worktreeInfoWatcher.send(.setActive(false)) },
.run { [clock] _ in
try await clock.sleep(for: .seconds(1))
await MainActor.run {
Expand All @@ -466,9 +482,15 @@
.cancellable(id: CancelID.backgroundPersist, cancelInFlight: true)
)
case .inactive:
return .cancel(id: CancelID.periodicRefresh)
return .merge(
.cancel(id: CancelID.periodicRefresh),
.run { _ in await worktreeInfoWatcher.send(.setActive(false)) }
)
@unknown default:
return .cancel(id: CancelID.periodicRefresh)
return .merge(
.cancel(id: CancelID.periodicRefresh),
.run { _ in await worktreeInfoWatcher.send(.setActive(false)) }
)
}

case .repositories(.delegate(.selectedWorktreeChanged(let worktree))):
Expand Down Expand Up @@ -704,6 +726,11 @@
.setPullRequestTrackingEnabled(settings.githubIntegrationEnabled)
)
},
.run { _ in
await worktreeInfoWatcher.send(
.setAutomaticRefreshEnabled(settings.automaticRepositoryRefreshEnabled)
)
},
.run { send in
guard shouldCheckSystemNotificationPermission else { return }
let status = await systemNotificationClient.authorizationStatus()
Expand All @@ -730,7 +757,7 @@
// refused policy switch would leave no surface at all. Fall back
// to the previous mode, which puts one of them back.
guard appLifecycleClient.applyVisibility(settings.appVisibility) else {
await send(.settings(.setAppVisibility(previousVisibility)))

Check warning on line 760 in supacode/Features/App/Reducer/AppFeature.swift

View workflow job for this annotation

GitHub Actions / build

no 'async' operations occur within 'await' expression
return
}
if dockIconReappeared {
Expand Down Expand Up @@ -1489,7 +1516,7 @@
return .send(.repositories(.selectArchivedWorktrees))

case .commandPalette(.delegate(.refreshWorktrees)):
return .send(.repositories(.refreshWorktrees))
return .send(.refreshWorktreesRequested)

case .commandPalette(.delegate(.ghosttyCommand(let action))):
guard let worktree = state.repositories.worktree(for: state.repositories.selectedWorktreeID) else {
Expand Down
Loading
Loading