native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
54 kB · 1300 lines
Swift
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301import AppKitimport ComposableArchitectureimport ProwlCLISharedimport Sharingimport SwiftUI
struct WorktreeDetailView: View { @Dependency(FeatureFlags.self) private var featureFlags private struct ToolbarSharedStateInput { let repositories: RepositoriesFeature.State let workflowRuns: WorkflowRunsFeature.State let actionTargetWorktree: Worktree? let notificationGroups: [ToolbarNotificationRepositoryGroup] let unseenNotificationWorktreeCount: Int let runScriptEnabled: Bool let runScriptIsRunning: Bool let customCommands: [EffectiveCustomCommand] let isUpdateAvailable: Bool let isUpdateReadyToInstall: Bool let availableUpdateVersion: String? let showRunButtonInToolbar: Bool }
@Bindable var store: StoreOf<AppFeature> let terminalManager: WorktreeTerminalManager @Environment(CommandKeyObserver.self) private var commandKeyObserver /// Drive the chrome (nav + toolbar) tint for Normal and Canvas modes. @Shared(.repositoryAppearances) private var repositoryAppearances @Shared(.settingsFile) private var settingsFile /// True while a Canvas card is expanded in place, so the otherwise-transparent /// Canvas toolbar gets a matching material scrim instead of showing through. @State private var isCanvasCardExpanded = false @State private var toolbarPopovers = ToolbarPopoverCoordinator() @State private var historyStore = Store(initialState: WorkflowStepHistoryFeature.State()) { WorkflowStepHistoryFeature() }
var body: some View { detailBody(state: store.state) }
private func detailBody(state: AppFeature.State) -> some View { let repositories = state.repositories let selectedRow = repositories.selectedRow(for: repositories.selectedWorktreeID) let selectedWorktree = repositories.worktree(for: repositories.selectedWorktreeID) let selectedTerminalWorktree = repositories.selectedTerminalWorktree let canvasFocusedTerminalWorktree = canvasFocusedTerminalWorktree(repositories: repositories) let actionTargetWorktree = selectedTerminalWorktree ?? canvasFocusedTerminalWorktree let historyContext = workflowHistoryContext(worktree: actionTargetWorktree, repositories: repositories) let selectedWorktreeSummaries = selectedWorktreeSummaries(from: repositories) let showsMultiSelectionSummary = shouldShowMultiSelectionSummary( repositories: repositories, selectedWorktreeSummaries: selectedWorktreeSummaries ) let loadingInfo = loadingInfo( for: selectedRow, selectedWorktreeID: repositories.selectedWorktreeID, repositories: repositories ) let hasActiveTerminalTarget = actionTargetWorktree != nil && loadingInfo == nil && !showsMultiSelectionSummary let runScriptEnabled = hasActiveTerminalTarget let runScriptIsRunning = actionTargetWorktree.flatMap { state.runScriptStatusByWorktreeID[$0.id] } == true let customCommands = state.selectedCustomCommands let notificationGroups = repositories.toolbarNotificationGroups( terminalManager: terminalManager, customTitles: repositories.repositoryCustomTitles ) let unseenNotificationWorktreeCount = notificationGroups.reduce(0) { count, repository in count + repository.unseenWorktreeCount } let sharedToolbarState = toolbarSharedState( input: ToolbarSharedStateInput( repositories: repositories, workflowRuns: state.workflowRuns, actionTargetWorktree: actionTargetWorktree, notificationGroups: notificationGroups, unseenNotificationWorktreeCount: unseenNotificationWorktreeCount, runScriptEnabled: runScriptEnabled, runScriptIsRunning: runScriptIsRunning, customCommands: customCommands, isUpdateAvailable: state.updates.isUpdateAvailable, isUpdateReadyToInstall: state.updates.isUpdateReadyToInstall, availableUpdateVersion: state.updates.availableVersion, showRunButtonInToolbar: settingsFile.global.showRunButtonInToolbar ) ) let content = detailContent( repositories: repositories, loadingInfo: loadingInfo, selectedWorktree: selectedWorktree, selectedTerminalWorktree: selectedTerminalWorktree, selectedWorktreeSummaries: selectedWorktreeSummaries ) .navigationTitle( WindowTitle.compute(repositories: repositories, terminalManager: terminalManager) ) .toolbar(removing: .title) .toolbar { detailToolbarContent( state: state, shared: sharedToolbarState, actionTargetWorktree: actionTargetWorktree, hasActiveTerminalTarget: hasActiveTerminalTarget) } .environment(historyStore) .environment(toolbarPopovers) .task { loadWorkflowHistory(context: historyContext, runs: state.workflowRuns.sessions.values.map(\.run)) } .onChange(of: historyContext) { _, context in historyStore.send(.context(context)) } .onChange(of: state.workflowRuns.sessions) { _, sessions in historyStore.send(.liveRuns(sessions.values.map(\.run))) } .windowToolbarChromeBackground( toolbarChromeFill(repositories: repositories), forceMaterialScrim: repositories.isShowingCanvas && isCanvasCardExpanded ) let actions = makeFocusedActions( repositories: repositories, hasActiveWorktree: hasActiveTerminalTarget, runScriptEnabled: runScriptEnabled, runScriptIsRunning: runScriptIsRunning ) let actionToken = WorktreeActionContext( selectedWorktreeID: selectedTerminalWorktree?.id, isShowingCanvas: repositories.isShowingCanvas, canvasFocusedWorktreeID: repositories.isShowingCanvas ? terminalManager.canvasFocusedWorktreeID : nil ) return applyFocusedActions(content: content, actions: actions, token: actionToken) }
@ToolbarContentBuilder private func detailToolbarContent( state: AppFeature.State, shared: ToolbarSharedState, actionTargetWorktree: Worktree?, hasActiveTerminalTarget: Bool ) -> some ToolbarContent { if state.repositories.isShowingCanvas { canvasToolbarContent(state: shared) } else if hasActiveTerminalTarget { worktreeToolbarContent( toolbarState: WorktreeToolbarState( shared: shared, openActionSelection: state.openActionSelection, openActionIsAutomatic: state.openActionIsAutomatic, showExtras: commandKeyObserver.isPressed, showDefaultEditorInToolbar: settingsFile.global.showDefaultEditorInToolbar ), actionTargetWorktree: actionTargetWorktree ) } else { if featureFlags.remoteMirror { ToolbarItem(placement: .navigation) { MirrorHostButton() } } } }
@ToolbarContentBuilder private func worktreeToolbarContent( toolbarState: WorktreeToolbarState, actionTargetWorktree: Worktree? ) -> some ToolbarContent { WorktreeToolbarContent( toolbarState: toolbarState, onOpenWorktree: { action in store.send(.openWorktree(action)) }, onOpenActionSelectionChanged: { action in store.send(.openActionSelectionChanged(action)) }, onResetOpenActionToAutomatic: { store.send(.openActionResetToAutomatic) }, onCopyPath: { guard let actionTargetWorktree else { return } NSPasteboard.general.clearContents() NSPasteboard.general.setString(actionTargetWorktree.workingDirectory.path, forType: .string) }, onSelectNotification: selectToolbarNotification, onDismissAllNotifications: { dismissAllToolbarNotifications(in: toolbarState.shared.notificationGroups) }, onRunScript: { store.send(.runScript) }, onStopRunScript: { store.send(.stopRunScript) }, onRunCustomCommand: { index in store.send(.runCustomCommand(index)) }, onActivateUpdateButton: { store.send(.updates(.activateUpdateButton)) }, onLaunchProfile: { store.send(.launchAgentProfile($0)) }, onManageProfiles: { store.send(.openAgentProfilesSettings) }, onManageWorkflows: { store.send(.openWorkflowSettings) }, onRunWorkflow: { key in store.send( .openWorkflowStart( workflowKey: key, worktreeID: nil, sourceSurfaceID: nil, forceSheet: false)) }, onRunWorkflowWithOptions: { key in store.send( .openWorkflowStart( workflowKey: key, worktreeID: nil, sourceSurfaceID: nil, forceSheet: true)) }, onShowWorkflowDetails: { item in guard let worktreeID = actionTargetWorktree?.id else { return } store.send(.openWorkflowDetails(item, worktreeID: worktreeID)) }, onWorkflowIntent: handleWorkflowIntent ) }
private func loadWorkflowHistory(context: WorkflowHistoryContext, runs: [WorkflowRun]) { historyStore.send(.context(context)) historyStore.send(.liveRuns(runs)) historyStore.send(.refresh) }
private func workflowHistoryContext(worktree: Worktree?, repositories: RepositoriesFeature.State) -> WorkflowHistoryContext { let pane = worktree.flatMap { terminalManager.stateIfExists(for: $0.id)?.currentFocusedSurfaceId() } let agent = repositories.activeAgents.entries.first { $0.surfaceID == pane } return WorkflowHistoryContext( paneID: pane, session: agent.flatMap { entry in WorkflowHistorySessionIdentity.resolve( agent: entry.agent, detected: entry.session, currentSignal: terminalManager.currentAgentSignalEvidenceSnapshot(surfaceID: entry.surfaceID) .latestManagedHook) }, worktreeID: worktree?.id, livePaneIDs: Set(terminalManager.activeWorktreeStates.flatMap { $0.surfaces.keys })) }
private func toolbarSharedState( input: ToolbarSharedStateInput ) -> ToolbarSharedState { ToolbarSharedState( actionTargetWorktreeID: input.actionTargetWorktree?.id, agentsCapsule: agentsCapsuleState(for: input.actionTargetWorktree), agentsLauncherItems: agentsLauncherItems(for: input.actionTargetWorktree), statusToast: input.repositories.statusToast, workflowStatus: WorkflowStatusCenterPresentation( state: input.workflowRuns, selectedWorktreeID: input.actionTargetWorktree?.id, now: Date() ), pullRequest: matchedPullRequest( for: input.actionTargetWorktree, repositories: input.repositories ), codeHost: input.repositories.codeHost(forWorktreeID: input.actionTargetWorktree?.id), notificationGroups: input.notificationGroups, unseenNotificationWorktreeCount: input.unseenNotificationWorktreeCount, runScriptEnabled: input.runScriptEnabled, runScriptIsRunning: input.runScriptIsRunning, customCommands: input.customCommands, isUpdateAvailable: input.isUpdateAvailable, isUpdateReadyToInstall: input.isUpdateReadyToInstall, availableUpdateVersion: input.availableUpdateVersion, showRunButtonInToolbar: input.showRunButtonInToolbar ) }
@ToolbarContentBuilder private func canvasToolbarContent( state: ToolbarSharedState ) -> some ToolbarContent { AgentNotificationsToolbarContent( onHistoryIntent: handleWorkflowIntent, agentsCapsule: state.agentsCapsule, agentsLauncherItems: state.agentsLauncherItems, notificationGroups: state.notificationGroups, unseenNotificationWorktreeCount: state.unseenNotificationWorktreeCount, workflowsWorktreeID: state.actionTargetWorktreeID, onLaunchProfile: { store.send(.launchAgentProfile($0)) }, onManageProfiles: { store.send(.openAgentProfilesSettings) }, onManageWorkflows: { store.send(.openWorkflowSettings) }, onRunWorkflow: { key in store.send( .openWorkflowStart( workflowKey: key, worktreeID: nil, sourceSurfaceID: nil, forceSheet: false)) }, onRunWorkflowWithOptions: { key in store.send( .openWorkflowStart( workflowKey: key, worktreeID: nil, sourceSurfaceID: nil, forceSheet: true)) }, onShowWorkflowDetails: { item in guard let worktreeID = state.actionTargetWorktreeID else { return } store.send(.openWorkflowDetails(item, worktreeID: worktreeID)) }, onSelectNotification: selectToolbarNotification, onDismissAllNotifications: { dismissAllToolbarNotifications(in: state.notificationGroups) }, isUpdateAvailable: state.isUpdateAvailable, isUpdateReadyToInstall: state.isUpdateReadyToInstall, availableUpdateVersion: state.availableUpdateVersion, onActivateUpdateButton: { store.send(.updates(.activateUpdateButton)) } )
ToolbarItem(placement: .principal) { ToolbarStatusView( toast: state.statusToast, workflow: state.workflowStatus, pullRequest: state.pullRequest, codeHost: state.codeHost, onWorkflowIntent: handleWorkflowIntent ) .padding(.horizontal) }
let showRunButton = state.showRunButtonInToolbar && (state.runScriptIsRunning || state.runScriptEnabled) let overflowCommands = Array(state.customCommands.enumerated().dropFirst(3)) // A fixed separator keeps the dynamic Run + Custom Command cluster distinct // from other trailing actions, mirroring the Normal toolbar spacing. // // INTENTIONAL DIVERGENCE FROM THE NORMAL TOOLBAR: the whole cluster is a // single `ToolbarItem` (an HStack) here, whereas `commandToolbarItems` // (Normal mode) lays the buttons out as separate items / a // `ToolbarItemGroup`. The reason is how each mode updates NSToolbar (which // SwiftUI's `.toolbar` bridges to): // - Normal: switching worktree swaps the whole detail view, so NSToolbar // is rebuilt wholesale — no per-item diff, no animation. // - Canvas: `CanvasView` stays mounted across card switches; only the // toolbar items change. With a multi-item structure NSToolbar performs // an incremental insert/remove with its own animation (which SwiftUI // transactions can't suppress), briefly overflowing the toolbar — the // visible "jump" when switching between cards with different command // counts. // Collapsing the cluster into one item keeps NSToolbar's item set stable, // so a command-count change is just an internal HStack relayout. Do NOT // "unify" this back into a `ToolbarItemGroup` to match Normal — that // reintroduces the jump. if showRunButton || !state.customCommands.isEmpty { ToolbarSpacer(.fixed) ToolbarItem(placement: .primaryAction) { // `spacing: 0` keeps the cluster as tight as the Normal toolbar's // ToolbarItemGroup (whose buttons sit nearly flush on macOS 26); the // buttons' own internal padding provides the visible gap. HStack(spacing: 0) { if showRunButton { RunScriptToolbarButton( isRunning: state.runScriptIsRunning, isEnabled: state.runScriptEnabled, runHelpText: AppShortcuts.helpText( title: "Run Script", commandID: AppShortcuts.CommandID.runScript, in: store.resolvedKeybindings ), stopHelpText: AppShortcuts.helpText( title: "Stop Script", commandID: AppShortcuts.CommandID.stopScript, in: store.resolvedKeybindings ), runShortcut: store.resolvedKeybindings.display(for: AppShortcuts.CommandID.runScript), stopShortcut: store.resolvedKeybindings.display( for: AppShortcuts.CommandID.stopScript), runAction: { store.send(.runScript) }, stopAction: { store.send(.stopRunScript) } ) } ForEach(Array(state.customCommands.enumerated().prefix(3)), id: \.element.id) { _, command in UserCustomCommandToolbarButton( title: command.command.resolvedTitle, systemImage: command.command.resolvedSystemImage, source: command.source, shortcut: store.resolvedKeybindings.display( for: command.keybindingID ), isEnabled: command.command.hasRunnableCommand, action: { store.send(.runCustomCommand(command.id)) } ) } if !overflowCommands.isEmpty { CustomCommandOverflowButton( entries: overflowCommands.map { $0.element }, shortcutDisplay: { command in store.resolvedKeybindings.display(for: command.keybindingID) }, onRunCustomCommand: { id in store.send(.runCustomCommand(id)) } ) } } } } }
/// The target pane's detected agent, feeding the Agents capsule. nil /// (no detected agent) renders the capsule in its generic form; launcher /// availability is resolved separately from the target worktree (docs-ai 053). private func agentsCapsuleState(for worktree: Worktree?) -> AgentsCapsuleState? { guard let worktree, let state = terminalManager.stateIfExists(for: worktree.id), let tabID = state.tabManager.selectedTabId, let surfaceID = state.activeSurfaceID(for: tabID), let paneState = state.surfaceAgentStates[surfaceID], let agent = paneState.detectedAgent else { return nil } let iconSource = paneState.iconLookupToken.flatMap(CommandIconMap.iconForFirstToken) ?? CommandIconMap.iconForFirstToken(agent.iconLookupToken) // Same naming rule as the Active Agents rows: the launch profile name // recorded at surface creation wins for Prowl-launched panes; launch // aliases such as `omp` show their own name, not the semantic agent's. // Runtime-gated like `configRoot`: a *different* agent started manually // in the same pane must not wear the old profile's name (docs-ai 053/005). let launchProfile = state.launchProfilesBySurface[surfaceID] let displayName = (launchProfile?.runtime.agent == agent ? launchProfile?.name : nil) ?? ActiveAgentEntry.displayName( iconLookupToken: paneState.iconLookupToken ?? agent.iconLookupToken, agent: agent ) return AgentsCapsuleState(displayName: displayName, iconSource: iconSource) }
/// Launchable profile rows for the Agents popover: the current worktree's /// Recommended profile first, then the remaining enabled profiles in list /// order. Availability (CLI installed) is presentation-only — a missing /// runtime grays the row with a reason instead of silently recommending /// another profile (docs-ai 053). private func agentsLauncherItems(for worktree: Worktree?) -> [AgentsLauncherItem] { @Shared(.userGlobalSettings) var globalSettings let profiles = globalSettings.agentProfiles guard let worktree, !profiles.isEmpty else { return [] } @Shared(.userRepositorySettings(worktree.repositoryRootURL)) var repositorySettings let recommendedID = AgentProfileRecommendation.recommendedProfile( profiles: profiles, designatedID: repositorySettings.defaultAgentProfileID, lastLaunchedID: repositorySettings.lastLaunchedAgentProfileID )?.id let enabled = profiles.filter(\.isEnabled) let ordered = enabled.sorted { lhs, rhs in (lhs.id == recommendedID ? 0 : 1) < (rhs.id == recommendedID ? 0 : 1) } return ordered.map { profile in AgentsLauncherItem( id: profile.id, name: profile.name, runtimeName: AgentRuntimeAdapterRegistry.displayName(for: profile.runtime), iconSource: profile.iconSource, isRecommended: profile.id == recommendedID, availabilityWarning: AgentProfileAvailability.launchWarning(for: profile) ) } }
private func selectedWorktreeSummaries( from repositories: RepositoriesFeature.State ) -> [MultiSelectedWorktreeSummary] { repositories.sidebarSelectedWorktreeIDs .compactMap { worktreeID in repositories.selectedRow(for: worktreeID).map { MultiSelectedWorktreeSummary( id: $0.id, name: $0.name, repositoryName: repositories.repositoryName(for: $0.repositoryID) ) } } .sorted { lhs, rhs in let lhsRepository = lhs.repositoryName ?? "" let rhsRepository = rhs.repositoryName ?? "" if lhsRepository == rhsRepository { return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending } return lhsRepository.localizedCaseInsensitiveCompare(rhsRepository) == .orderedAscending } }
private func matchedPullRequest( for worktree: Worktree?, repositories: RepositoriesFeature.State ) -> GithubPullRequest? { guard let worktree, let pullRequest = repositories.worktreeInfo(for: worktree.id)?.pullRequest else { return nil } guard pullRequest.headRefName == nil || pullRequest.headRefName == worktree.name else { return nil } return pullRequest }
private func shouldShowMultiSelectionSummary( repositories: RepositoriesFeature.State, selectedWorktreeSummaries: [MultiSelectedWorktreeSummary] ) -> Bool { !repositories.isShowingArchivedWorktrees && !repositories.isShowingCanvas && selectedWorktreeSummaries.count > 1 }
private func canvasFocusedTerminalWorktree(repositories: RepositoriesFeature.State) -> Worktree? { guard repositories.isShowingCanvas, let worktreeID = terminalManager.canvasFocusedWorktreeID else { return nil } if let worktree = repositories.worktree(for: worktreeID) { return worktree } guard let repository = repositories.repositories[id: worktreeID], repository.capabilities.supportsRunnableFolderActions, !repository.capabilities.supportsWorktrees else { return nil } return Worktree( id: repository.id, name: repository.name, detail: repository.rootURL.path(percentEncoded: false), workingDirectory: repository.rootURL, repositoryRootURL: repository.rootURL ) }
@ViewBuilder private func detailContent( repositories: RepositoriesFeature.State, loadingInfo: WorktreeLoadingInfo?, selectedWorktree: Worktree?, selectedTerminalWorktree: Worktree?, selectedWorktreeSummaries: [MultiSelectedWorktreeSummary] ) -> some View { if repositories.isShowingCanvas { CanvasView( terminalManager: terminalManager, repositoryCustomTitles: repositories.repositoryCustomTitles, focusRequest: repositories.pendingCanvasFocusRequest, commandRequest: repositories.pendingCanvasCommandRequest, onFocusedWorktreeChanged: { worktreeID in store.send(.canvasFocusedWorktreeChanged(worktreeID)) }, onFocusRequestConsumed: { requestID in store.send(.repositories(.consumeCanvasFocusRequest(requestID))) }, onCommandConsumed: { requestID in store.send(.repositories(.consumeCanvasCommandRequest(requestID))) }, onExpandedChange: { expanded in isCanvasCardExpanded = expanded } ) // Canvas tints the nav (leading) only; the toolbar is left untinted so // floating cards don't read against a colored band. The card title // bars still carry their own per-repo color. .windowChromeTint(chromeFill(repositories: repositories, context: .canvas), edges: [.leading]) } else if repositories.isShowingShelf { // Shelf manages its own chrome bands (and its always-repo-colored // spine) inside `ShelfView`, so no tint modifier is applied here. ShelfView( store: store.scope(state: \.repositories, action: \.repositories), terminalManager: terminalManager, createTab: { store.send(.newTerminal) } ) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { // Normal view mode (terminal, archived list, multi-selection, loading, // repository detail, empty): tint the toolbar (top) and nav (leading) // chrome, and pass the same fill into the terminal tab bar so its // background reads as part of the same tinted chrome. let normalFill = chromeFill(repositories: repositories, context: .normal) normalModeContent( repositories: repositories, loadingInfo: loadingInfo, selectedTerminalWorktree: selectedTerminalWorktree, selectedWorktreeSummaries: selectedWorktreeSummaries, barTint: normalFill ) .windowChromeTint(normalFill, edges: [.top, .leading]) } }
@ViewBuilder private func normalModeContent( repositories: RepositoriesFeature.State, loadingInfo: WorktreeLoadingInfo?, selectedTerminalWorktree: Worktree?, selectedWorktreeSummaries: [MultiSelectedWorktreeSummary], barTint: WindowChromeTint.Fill? ) -> some View { if repositories.isShowingArchivedWorktrees { ArchivedWorktreesDetailView( store: store.scope(state: \.repositories, action: \.repositories) ) } else if shouldShowMultiSelectionSummary( repositories: repositories, selectedWorktreeSummaries: selectedWorktreeSummaries ) { MultiSelectedWorktreesDetailView(rows: selectedWorktreeSummaries) } else if let loadingInfo { WorktreeLoadingView(info: loadingInfo) } else if let selectedTerminalWorktree { let shouldRunSetupScript = repositories.pendingSetupScriptWorktreeIDs.contains( selectedTerminalWorktree.id) let shouldFocusTerminal = repositories.shouldFocusTerminal(for: selectedTerminalWorktree.id) WorktreeTerminalTabsView( worktree: selectedTerminalWorktree, manager: terminalManager, shouldRunSetupScript: shouldRunSetupScript, forceAutoFocus: shouldFocusTerminal, createTab: { store.send(.newTerminal) }, barTint: barTint ) .id(selectedTerminalWorktree.id) .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { if shouldFocusTerminal { store.send( .repositories(.worktreeCreation(.consumeTerminalFocus(selectedTerminalWorktree.id)))) } } } else if let selectedRepository = repositories.selectedRepository { RepositoryDetailView( repository: selectedRepository, customTitle: repositories.repositoryCustomTitles[selectedRepository.id], onEditWorkspace: { store.send( .repositories(.workspaceEditing(.promptRequested(selectedRepository.id, removingChildID: nil)))) } ) } else { EmptyStateView(store: store.scope(state: \.repositories, action: \.repositories)) } }
/// The chrome region a tint is being resolved for. private enum ChromeContext { case normal case canvas }
/// Resolves the chrome band fill for the current view mode, honoring the /// user's window tint setting. In `.repositoryColor` mode the band tracks /// the active repository — the selected worktree's repo in Normal, the /// focused card's repo in Canvas — falling back to a neutral surface when /// none is colored. private func chromeFill( repositories: RepositoriesFeature.State, context: ChromeContext ) -> WindowChromeTint.Fill? { let repositoryID: Repository.ID? = switch context { case .normal: repositories.repositoryID(for: repositories.selectedWorktreeID) ?? repositories.selectedRepositoryID case .canvas: repositories.repositoryID(for: terminalManager.canvasFocusedWorktreeID) } let repositoryColor = repositoryID.flatMap { repositoryAppearances[$0]?.color } return WindowChromeTint.fill( mode: settingsFile.global.windowTintMode, customColor: settingsFile.global.windowTintCustomColor.color, repositoryColor: repositoryColor ) }
/// Resolves the real window toolbar background. Unlike the content tint /// bands, this applies to the AppKit/SwiftUI toolbar surface itself, which /// remains visible when macOS changes the zoomed/fullscreen window layout. private func toolbarChromeFill(repositories: RepositoriesFeature.State) -> WindowChromeTint.Fill? { guard !repositories.isShowingCanvas else { return nil } return chromeFill(repositories: repositories, context: .normal) }
private func applyFocusedActions<Content: View>( content: Content, actions: FocusedActions, token: WorktreeActionContext ) -> some View { content .focusedSceneValue( \.openSelectedWorktreeAction, actions.openSelectedWorktree.asFocusedAction(token: token) ) .focusedSceneValue(\.newTerminalAction, actions.newTerminal.asFocusedAction(token: token)) .focusedSceneValue(\.closeTabAction, actions.closeTab.asFocusedAction(token: token)) .focusedSceneValue(\.closeSurfaceAction, actions.closeSurface.asFocusedAction(token: token)) .focusedSceneValue(\.resetFontSizeAction, actions.resetFontSize.asFocusedAction(token: token)) .focusedSceneValue( \.increaseFontSizeAction, actions.increaseFontSize.asFocusedAction(token: token) ) .focusedSceneValue( \.decreaseFontSizeAction, actions.decreaseFontSize.asFocusedAction(token: token) ) .focusedSceneValue(\.startSearchAction, actions.startSearch.asFocusedAction(token: token)) .focusedSceneValue( \.searchSelectionAction, actions.searchSelection.asFocusedAction(token: token) ) .focusedSceneValue( \.navigateSearchNextAction, actions.navigateSearchNext.asFocusedAction(token: token) ) .focusedSceneValue( \.navigateSearchPreviousAction, actions.navigateSearchPrevious.asFocusedAction(token: token) ) .focusedSceneValue(\.endSearchAction, actions.endSearch.asFocusedAction(token: token)) .focusedSceneValue( \.selectPreviousTerminalTabAction, actions.selectPreviousTerminalTab.asFocusedAction(token: token) ) .focusedSceneValue( \.selectNextTerminalTabAction, actions.selectNextTerminalTab.asFocusedAction(token: token) ) .focusedSceneValue( \.selectPreviousTerminalPaneAction, actions.selectPreviousTerminalPane.asFocusedAction(token: token) ) .focusedSceneValue( \.selectNextTerminalPaneAction, actions.selectNextTerminalPane.asFocusedAction(token: token) ) .focusedSceneValue( \.selectTerminalPaneAboveAction, actions.selectTerminalPaneAbove.asFocusedAction(token: token) ) .focusedSceneValue( \.selectTerminalPaneBelowAction, actions.selectTerminalPaneBelow.asFocusedAction(token: token) ) .focusedSceneValue( \.selectTerminalPaneLeftAction, actions.selectTerminalPaneLeft.asFocusedAction(token: token) ) .focusedSceneValue( \.selectTerminalPaneRightAction, actions.selectTerminalPaneRight.asFocusedAction(token: token) ) .focusedSceneValue(\.runScriptAction, actions.runScript.asFocusedAction(token: token)) .focusedSceneValue(\.stopRunScriptAction, actions.stopRunScript.asFocusedAction(token: token)) }
private func makeFocusedActions( repositories: RepositoriesFeature.State, hasActiveWorktree: Bool, runScriptEnabled: Bool, runScriptIsRunning: Bool ) -> FocusedActions { func action(_ appAction: AppFeature.Action) -> (() -> Void)? { hasActiveWorktree ? { store.send(appAction) } : nil }
func canvasAction(_ perform: @escaping (WorktreeTerminalState) -> Bool) -> (() -> Void)? { guard repositories.isShowingCanvas else { return nil } return { guard let worktreeID = terminalManager.canvasFocusedWorktreeID, let state = terminalManager.stateIfExists(for: worktreeID) else { return } _ = perform(state) } }
func fontSizeAction(_ bindingAction: String) -> (() -> Void)? { if repositories.isShowingCanvas { return { guard let worktreeID = terminalManager.canvasFocusedWorktreeID, let state = terminalManager.stateIfExists(for: worktreeID) else { return } _ = state.performBindingActionOnFocusedSurface(bindingAction) terminalManager.syncPreferredFontSize(from: worktreeID) } } guard hasActiveWorktree, let selectedWorktree = repositories.selectedTerminalWorktree else { return nil } return { guard let state = terminalManager.stateIfExists(for: selectedWorktree.id) else { return } _ = state.performBindingActionOnFocusedSurface(bindingAction) terminalManager.syncPreferredFontSize(from: selectedWorktree.id) } }
func terminalBindingAction(_ bindingAction: String) -> (() -> Void)? { if let action = canvasAction({ $0.performBindingActionOnFocusedSurface(bindingAction) }) { return action } guard hasActiveWorktree, let selectedWorktree = repositories.selectedTerminalWorktree else { return nil } return { guard let state = terminalManager.stateIfExists(for: selectedWorktree.id) else { return } _ = state.performBindingActionOnFocusedSurface(bindingAction) } }
return FocusedActions( openSelectedWorktree: action(.openSelectedWorktree), newTerminal: action(.newTerminal), closeTab: closeTabFocusedAction( repositories: repositories, hasActiveWorktree: hasActiveWorktree), closeSurface: closeSurfaceFocusedAction( repositories: repositories, hasActiveWorktree: hasActiveWorktree), resetFontSize: fontSizeAction("reset_font_size"), increaseFontSize: fontSizeAction("increase_font_size:1"), decreaseFontSize: fontSizeAction("decrease_font_size:1"), startSearch: action(.startSearch), searchSelection: action(.searchSelection), navigateSearchNext: action(.navigateSearchNext), navigateSearchPrevious: action(.navigateSearchPrevious), endSearch: action(.endSearch), selectPreviousTerminalTab: terminalBindingAction("previous_tab"), selectNextTerminalTab: terminalBindingAction("next_tab"), selectPreviousTerminalPane: terminalBindingAction("goto_split:previous"), selectNextTerminalPane: terminalBindingAction("goto_split:next"), selectTerminalPaneAbove: terminalBindingAction("goto_split:up"), selectTerminalPaneBelow: terminalBindingAction("goto_split:down"), selectTerminalPaneLeft: terminalBindingAction("goto_split:left"), selectTerminalPaneRight: terminalBindingAction("goto_split:right"), runScript: runScriptEnabled ? { store.send(.runScript) } : nil, stopRunScript: runScriptIsRunning ? { store.send(.stopRunScript) } : nil ) }
private func closeTabFocusedAction( repositories: RepositoriesFeature.State, hasActiveWorktree: Bool ) -> (() -> Void)? { if repositories.isShowingCanvas { guard let worktreeID = terminalManager.canvasFocusedWorktreeID, let state = terminalManager.stateIfExists(for: worktreeID), state.canCloseFocusedTab else { return nil } return { _ = state.closeFocusedTab() } } guard hasActiveWorktree, let selectedWorktree = repositories.selectedTerminalWorktree, terminalManager.stateIfExists(for: selectedWorktree.id)?.canCloseFocusedTab == true else { return nil } return { store.send(.closeTab) } }
private func closeSurfaceFocusedAction( repositories: RepositoriesFeature.State, hasActiveWorktree: Bool ) -> (() -> Void)? { if repositories.isShowingCanvas { guard let worktreeID = terminalManager.canvasFocusedWorktreeID, let state = terminalManager.stateIfExists(for: worktreeID), state.canCloseFocusedSurface else { return nil } return { _ = state.closeFocusedSurface() } } guard hasActiveWorktree, let selectedWorktree = repositories.selectedTerminalWorktree, terminalManager.stateIfExists(for: selectedWorktree.id)?.canCloseFocusedSurface == true else { return nil } return { store.send(.closeSurface) } }
private func selectToolbarNotification( _ worktreeID: Worktree.ID, _ notification: WorktreeTerminalNotification ) { if let runID = notification.workflowRunID { terminalManager.markNotificationRead(worktreeID: worktreeID, notificationID: notification.id) historyStore.send(.setPresented(true)) historyStore.send(.select(runID)) historyStore.send(.openRequested) return } store.send(.repositories(.selectWorktree(worktreeID))) if let terminalState = terminalManager.stateIfExists(for: worktreeID) { _ = terminalState.focusSurface(id: notification.surfaceId) } }
private func dismissAllToolbarNotifications(in groups: [ToolbarNotificationRepositoryGroup]) { for repositoryGroup in groups { for worktreeGroup in repositoryGroup.worktrees { terminalManager.stateIfExists(for: worktreeGroup.id)?.dismissAllNotifications() } } }
private func handleWorkflowIntent(_ intent: WorkflowRunPanelIntent) { switch intent { case .focusPane(let worktreeID, let surfaceID): store.send(.repositories(.selectWorktree(worktreeID))) _ = terminalManager.stateIfExists(for: worktreeID)?.focusSurface(id: surfaceID) case .userAction(let runID, let action): store.send(.workflowRuns(.userAction(runID: runID, action))) case .revealRunFolder(let url): NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path) case .openLog(let url): NSWorkspace.shared.open(url) } }
/// Hashable identity of the inputs the focused actions capture, used as the /// `FocusedAction` token. The detail body re-runs on every OSC-9 progress /// tick during agent activity; without a stable token each run would look /// like a focused-value change and rebuild the menu bar. Including the /// selected / canvas-focused worktree here keeps the published actions stable /// while the same worktree is focused, yet still republishes when the target /// worktree changes (so a menu item never fires against a stale worktree). private struct WorktreeActionContext: Hashable { let selectedWorktreeID: Worktree.ID? let isShowingCanvas: Bool let canvasFocusedWorktreeID: Worktree.ID? }
private struct FocusedActions { let openSelectedWorktree: (() -> Void)? let newTerminal: (() -> Void)? let closeTab: (() -> Void)? let closeSurface: (() -> Void)? let resetFontSize: (() -> Void)? let increaseFontSize: (() -> Void)? let decreaseFontSize: (() -> Void)? let startSearch: (() -> Void)? let searchSelection: (() -> Void)? let navigateSearchNext: (() -> Void)? let navigateSearchPrevious: (() -> Void)? let endSearch: (() -> Void)? let selectPreviousTerminalTab: (() -> Void)? let selectNextTerminalTab: (() -> Void)? let selectPreviousTerminalPane: (() -> Void)? let selectNextTerminalPane: (() -> Void)? let selectTerminalPaneAbove: (() -> Void)? let selectTerminalPaneBelow: (() -> Void)? let selectTerminalPaneLeft: (() -> Void)? let selectTerminalPaneRight: (() -> Void)? let runScript: (() -> Void)? let stopRunScript: (() -> Void)? }
struct ToolbarSharedState { let actionTargetWorktreeID: Worktree.ID? let agentsCapsule: AgentsCapsuleState? let agentsLauncherItems: [AgentsLauncherItem] let statusToast: RepositoriesFeature.StatusToast? let workflowStatus: WorkflowStatusCenterPresentation let pullRequest: GithubPullRequest? let codeHost: CodeHost let notificationGroups: [ToolbarNotificationRepositoryGroup] let unseenNotificationWorktreeCount: Int let runScriptEnabled: Bool let runScriptIsRunning: Bool let customCommands: [EffectiveCustomCommand] let isUpdateAvailable: Bool let isUpdateReadyToInstall: Bool let availableUpdateVersion: String? let showRunButtonInToolbar: Bool }
struct WorktreeToolbarState { let shared: ToolbarSharedState let openActionSelection: OpenWorktreeAction let openActionIsAutomatic: Bool let showExtras: Bool let showDefaultEditorInToolbar: Bool }
/// Shared leading toolbar cluster for Normal, Shelf, and Canvas. Agents and /// Quick Launch share one native group; notifications and update status share /// the trailing group that replaces the former branch item. struct AgentNotificationsToolbarContent: ToolbarContent { @Environment(StoreOf<WorkflowStepHistoryFeature>.self) private var historyStore @Dependency(FeatureFlags.self) private var featureFlags let onHistoryIntent: (WorkflowRunPanelIntent) -> Void
let agentsCapsule: AgentsCapsuleState? let agentsLauncherItems: [AgentsLauncherItem] let notificationGroups: [ToolbarNotificationRepositoryGroup] let unseenNotificationWorktreeCount: Int let workflowsWorktreeID: Worktree.ID? let onLaunchProfile: (AgentProfile.ID) -> Void let onManageProfiles: () -> Void let onManageWorkflows: () -> Void let onRunWorkflow: (String) -> Void let onRunWorkflowWithOptions: (String) -> Void let onShowWorkflowDetails: (WorkflowStartCatalogItem) -> Void let onSelectNotification: (Worktree.ID, WorktreeTerminalNotification) -> Void let onDismissAllNotifications: () -> Void let isUpdateAvailable: Bool let isUpdateReadyToInstall: Bool let availableUpdateVersion: String? let onActivateUpdateButton: () -> Void
var body: some ToolbarContent { ToolbarItemGroup(placement: .navigation) { AgentsToolbarButton( capsule: agentsCapsule, launcherItems: agentsLauncherItems, workflowsWorktreeID: workflowsWorktreeID, onLaunchProfile: onLaunchProfile, onManageProfiles: onManageProfiles, onManageWorkflows: onManageWorkflows, onRunWorkflow: onRunWorkflow, onRunWorkflowWithOptions: onRunWorkflowWithOptions, onShowWorkflowDetails: onShowWorkflowDetails ) if let quickLaunchItem = agentsLauncherItems.first { AgentsQuickLaunchButton(item: quickLaunchItem, onLaunch: onLaunchProfile) } }
// Adjacent navigation groups merge on macOS 26. This isolated item owns // the second capsule; see docs-ai 061 before changing its structure. ToolbarItem(placement: .navigation) { HStack(spacing: 0) { ToolbarNotificationsPopoverButton( groups: notificationGroups, unseenWorktreeCount: unseenNotificationWorktreeCount, onSelectNotification: onSelectNotification, onDismissAll: onDismissAllNotifications ) if featureFlags.workflowUI && !historyStore.entries.isEmpty { WorkflowHistoryPopoverButton(store: historyStore, onIntent: onHistoryIntent) } if featureFlags.remoteMirror { MirrorHostButton() } if isUpdateAvailable { ToolbarUpdateButton( availableVersion: availableUpdateVersion, isReadyToInstall: isUpdateReadyToInstall, onActivate: onActivateUpdateButton ) } } .glassEffect(.regular.interactive(), in: Capsule()) } .sharedBackgroundVisibility(.hidden) } }
struct WorktreeToolbarContent: ToolbarContent { let toolbarState: WorktreeToolbarState let onOpenWorktree: (OpenWorktreeAction) -> Void let onOpenActionSelectionChanged: (OpenWorktreeAction) -> Void let onResetOpenActionToAutomatic: () -> Void let onCopyPath: () -> Void let onSelectNotification: (Worktree.ID, WorktreeTerminalNotification) -> Void let onDismissAllNotifications: () -> Void let onRunScript: () -> Void let onStopRunScript: () -> Void let onRunCustomCommand: (EffectiveCustomCommand.Identifier) -> Void let onActivateUpdateButton: () -> Void let onLaunchProfile: (AgentProfile.ID) -> Void let onManageProfiles: () -> Void let onManageWorkflows: () -> Void let onRunWorkflow: (String) -> Void let onRunWorkflowWithOptions: (String) -> Void let onShowWorkflowDetails: (WorkflowStartCatalogItem) -> Void let onWorkflowIntent: (WorkflowRunPanelIntent) -> Void @Environment(\.resolvedKeybindings) private var resolvedKeybindings
var body: some ToolbarContent { AgentNotificationsToolbarContent( onHistoryIntent: onWorkflowIntent, agentsCapsule: toolbarState.shared.agentsCapsule, agentsLauncherItems: toolbarState.shared.agentsLauncherItems, notificationGroups: toolbarState.shared.notificationGroups, unseenNotificationWorktreeCount: toolbarState.shared.unseenNotificationWorktreeCount, workflowsWorktreeID: toolbarState.shared.actionTargetWorktreeID, onLaunchProfile: onLaunchProfile, onManageProfiles: onManageProfiles, onManageWorkflows: onManageWorkflows, onRunWorkflow: onRunWorkflow, onRunWorkflowWithOptions: onRunWorkflowWithOptions, onShowWorkflowDetails: onShowWorkflowDetails, onSelectNotification: onSelectNotification, onDismissAllNotifications: onDismissAllNotifications, isUpdateAvailable: toolbarState.shared.isUpdateAvailable, isUpdateReadyToInstall: toolbarState.shared.isUpdateReadyToInstall, availableUpdateVersion: toolbarState.shared.availableUpdateVersion, onActivateUpdateButton: onActivateUpdateButton )
ToolbarItem(placement: .principal) { ToolbarStatusView( toast: toolbarState.shared.statusToast, workflow: toolbarState.shared.workflowStatus, pullRequest: toolbarState.shared.pullRequest, codeHost: toolbarState.shared.codeHost, onWorkflowIntent: onWorkflowIntent ) .padding(.horizontal) }
if toolbarState.showDefaultEditorInToolbar { ToolbarSpacer(.fixed) ToolbarItemGroup { openMenu( openActionSelection: toolbarState.openActionSelection, openActionIsAutomatic: toolbarState.openActionIsAutomatic, showExtras: toolbarState.showExtras ) } } commandToolbarItems
}
@ViewBuilder private func openMenu( openActionSelection: OpenWorktreeAction, openActionIsAutomatic: Bool, showExtras: Bool ) -> some View { let availableActions = OpenWorktreeAction.availableCases let resolvedOpenActionSelection = OpenWorktreeAction.availableSelection(openActionSelection) Button { onOpenWorktree(resolvedOpenActionSelection) } label: { OpenWorktreeActionMenuLabelView( action: resolvedOpenActionSelection, shortcutHint: showExtras ? shortcutDisplay(for: AppShortcuts.CommandID.openWorktree) : nil ) } .help(openActionHelpText(for: resolvedOpenActionSelection, isDefault: true))
Menu { Button { onResetOpenActionToAutomatic() } label: { if openActionIsAutomatic { Label("Automatic", systemImage: "checkmark") } else { Text("Automatic") } } .buttonStyle(.plain) .help("Pick the app automatically based on the project type") Divider() ForEach(availableActions) { action in let isDefault = action == resolvedOpenActionSelection Button { onOpenActionSelectionChanged(action) onOpenWorktree(action) } label: { OpenWorktreeActionMenuLabelView(action: action, shortcutHint: nil) } .buttonStyle(.plain) .help(openActionHelpText(for: action, isDefault: isDefault)) } Divider() Button("Copy Path") { onCopyPath() } .help("Copy path") } label: { Image(systemName: "chevron.down") .font(.caption2) .accessibilityLabel("Open in menu") } .imageScale(.small) .menuIndicator(.hidden) .fixedSize() .help("Open in...")
}
private func openActionHelpText(for action: OpenWorktreeAction, isDefault: Bool) -> String { guard isDefault else { return action.title } return AppShortcuts.helpText( title: LocalizedStringResource(runtimeKey: action.title), commandID: AppShortcuts.CommandID.openWorktree, in: resolvedKeybindings ) }
@ToolbarContentBuilder private var commandToolbarItems: some ToolbarContent { let showRunButton = toolbarState.shared.showRunButtonInToolbar && (toolbarState.shared.runScriptIsRunning || toolbarState.shared.runScriptEnabled) let entries = customCommandEntries let inlineEntries = Array(entries.prefix(3)) let overflowEntries = Array(entries.dropFirst(3))
// One fixed separator in front of the whole Run + Custom Command cluster // keeps it distinct from preceding trailing actions no matter which items // are hidden. Run and the custom commands share one group (no spacer // between them), matching the grouping before the toolbar toggles. if showRunButton || !inlineEntries.isEmpty || !overflowEntries.isEmpty { ToolbarSpacer(.fixed) }
if showRunButton { ToolbarItem { RunScriptToolbarButton( isRunning: toolbarState.shared.runScriptIsRunning, isEnabled: toolbarState.shared.runScriptEnabled, runHelpText: AppShortcuts.helpText( title: "Run Script", commandID: AppShortcuts.CommandID.runScript, in: resolvedKeybindings ), stopHelpText: AppShortcuts.helpText( title: "Stop Script", commandID: AppShortcuts.CommandID.stopScript, in: resolvedKeybindings ), runShortcut: shortcutDisplay(for: AppShortcuts.CommandID.runScript), stopShortcut: shortcutDisplay(for: AppShortcuts.CommandID.stopScript), runAction: onRunScript, stopAction: onStopRunScript ) } }
if !inlineEntries.isEmpty { ToolbarItemGroup { ForEach(inlineEntries) { entry in customCommandButton(entry) } } }
if !overflowEntries.isEmpty { ToolbarItem { CustomCommandOverflowButton( entries: overflowEntries, shortcutDisplay: customCommandShortcutDisplay(for:), onRunCustomCommand: onRunCustomCommand ) } } }
private var customCommandEntries: [EffectiveCustomCommand] { toolbarState.shared.customCommands }
private func customCommandButton(_ command: EffectiveCustomCommand) -> some View { UserCustomCommandToolbarButton( title: command.command.resolvedTitle, systemImage: command.command.resolvedSystemImage, source: command.source, shortcut: customCommandShortcutDisplay(for: command), isEnabled: command.command.hasRunnableCommand, action: { onRunCustomCommand(command.id) } ) }
private func customCommandShortcutDisplay(for command: EffectiveCustomCommand) -> String? { shortcutDisplay(for: command.keybindingID) }
private func shortcutDisplay(for commandID: String) -> String? { AppShortcuts.display(for: commandID, in: resolvedKeybindings) } }
private func loadingInfo( for selectedRow: WorktreeRowModel?, selectedWorktreeID: Worktree.ID?, repositories: RepositoriesFeature.State ) -> WorktreeLoadingInfo? { guard let selectedRow else { return nil } let repositoryName = repositories.repositoryName(for: selectedRow.repositoryID) let isFolder = repositories.repositories[id: selectedRow.repositoryID]?.kind == .plain if selectedRow.isDeleting { return WorktreeLoadingInfo( name: selectedRow.name, repositoryName: repositoryName, state: .removing, isFolder: isFolder, statusTitle: nil, statusDetail: nil, statusCommand: nil, statusLines: [] ) } if selectedRow.isArchiving { let progress = repositories.archiveScriptProgress(for: selectedWorktreeID) return WorktreeLoadingInfo( name: selectedRow.name, repositoryName: repositoryName, state: .archiving, statusTitle: progress?.titleText ?? selectedRow.name, statusDetail: progress?.detailText ?? selectedRow.detail, statusCommand: progress?.commandText, statusLines: progress?.outputLines ?? [] ) } if selectedRow.isPending { let pending = repositories.pendingWorktree(for: selectedWorktreeID) let progress = pending?.progress let displayName = progress?.worktreeName ?? selectedRow.name return WorktreeLoadingInfo( name: displayName, repositoryName: repositoryName, state: .creating, statusTitle: progress?.titleText ?? selectedRow.name, statusDetail: progress?.detailText ?? selectedRow.detail, statusCommand: progress?.commandText, statusLines: progress?.liveOutputLines ?? [] ) } return nil }}