Skip to content
Open
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: 6 additions & 0 deletions Scripts/Fixtures/test-suite-contract-ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2823,3 +2823,9 @@ root/RepoPromptTests.AgentNavigationHUDSnapshotBuilderTests/testSearchTokensSpli
root/RepoPromptTests.AgentModeSidebarNavigationTests/testParentCyclingUsesRootRowsAndMapsChildToNearestRoot root Tests/RepoPromptTests/AgentMode/AgentModeSidebarNavigationTests.swift RepoPromptTests.AgentModeSidebarNavigationTests testParentCyclingUsesRootRowsAndMapsChildToNearestRoot AgentMode/Sidebar agent_sidebar_navigation.parent_cycle_child_to_root keyboard_navigation,subagents,parent_sessions deterministic_regression root_swiftpm 2 Parent-session cycling from child rows advances to the neighboring root row in both forward and backward directions. Keyboard cycling could unexpectedly land on subagents or skip the intended parent session groups. test_case retain 0 PR #352 retained for user-visible keyboard navigation over nested sessions.
root/RepoPromptTests.AgentModeSidebarNavigationTests/testParentCyclingWrapsAndFallsBackWithoutActiveTab root Tests/RepoPromptTests/AgentMode/AgentModeSidebarNavigationTests.swift RepoPromptTests.AgentModeSidebarNavigationTests testParentCyclingWrapsAndFallsBackWithoutActiveTab AgentMode/Sidebar agent_sidebar_navigation.parent_cycle_wrap_fallback keyboard_navigation,wraparound,no_active_tab deterministic_regression root_swiftpm 3 Parent-session cycling wraps at list boundaries and chooses a deterministic first/last parent when no active tab is available. Global navigation shortcuts could fail or pick an arbitrary target when focus state is absent or at a boundary. test_case retain 0 PR #352 retained for boundary behavior of sidebar keyboard navigation.
root/RepoPromptTests.AppPlatformUtilityRecoveryTests/testInAppAgentSessionRouteReturnsResultWithoutQueueingURL root Tests/RepoPromptTests/App/AppPlatformUtilityRecoveryTests.swift RepoPromptTests.AppPlatformUtilityRecoveryTests testInAppAgentSessionRouteReturnsResultWithoutQueueingURL App/Routing app_deep_link.agent_session_in_app_no_url_queue agent_session_route,in_app_routing,workspace_unavailable deterministic_regression root_swiftpm 1 In-app agent-session routing returns workspaceUnavailable without queueing a URL when no live windows are registered. HUD session jumps could enqueue ephemeral agent-session URLs and replay stale navigation later. shared_singleton WindowStatesManager test_case+defer_restore retain 0 PR #352 retained as a small side-effect regression for non-URL in-app routing.
root/RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests/testWorkspaceRootCodemapActivityMapsAcceptedProgressEvents root Tests/RepoPromptTests/AgentMode/AgentWorkspaceRootsSidebarStoreTests.swift RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests testWorkspaceRootCodemapActivityMapsAcceptedProgressEvents AgentMode agent_sidebar.codemap_progress.event_to_activity_mapping codemap_progress,segment,seal,catalog_completion deterministic_unit root_swiftpm 4 Accepted progress events map to determinate scanning with catalog-completion totals, indeterminate scanning without totals, and ready for sealed or complete-phase events. A wrong mapping could show indefinite scanning or premature ready for roots whose projection state differs. direct_in_process test_case retain 0 Per-root codemap sidebar indicator: event-to-activity mapping contract.
root/RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests/testCodemapProgressStatesJoinActivityByRootUUIDOnlyAndDescribeStates root Tests/RepoPromptTests/AgentMode/AgentWorkspaceRootsSidebarStoreTests.swift RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests testCodemapProgressStatesJoinActivityByRootUUIDOnlyAndDescribeStates AgentMode agent_sidebar.codemap_progress.root_uuid_join codemap_progress,root_identity,worktree_non_cross_wiring,display_text,accessibility deterministic_unit root_swiftpm 4 Progress states join strictly by visible root UUID: hidden physical worktree root activity is dropped, idle roots render nothing, and display/accessibility texts match each state. Path-based or loose joining could cross-wire hidden worktree scan progress onto visible logical root rows. direct_in_process test_case retain 0 Per-root codemap sidebar indicator: UUID-only join and state texts.
root/RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests/testCodemapProgressStatesGlobalDisableOverridesActivity root Tests/RepoPromptTests/AgentMode/AgentWorkspaceRootsSidebarStoreTests.swift RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests testCodemapProgressStatesGlobalDisableOverridesActivity AgentMode agent_sidebar.codemap_progress.global_disable_override codemap_progress,global_disable deterministic_unit root_swiftpm 2 With the global code maps kill-switch on, every visible root maps to the disabled state regardless of per-root activity presence. Scan progress could keep rendering while code maps are globally disabled, contradicting the app-wide setting. direct_in_process test_case retain 0 Per-root codemap sidebar indicator: global disable wins.
root/RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests/testCodemapActivityUpdatesPreserveRowIdentityAndGlobalDisableRoundTrips root Tests/RepoPromptTests/AgentMode/AgentWorkspaceRootsSidebarStoreTests.swift RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests testCodemapActivityUpdatesPreserveRowIdentityAndGlobalDisableRoundTrips AgentMode agent_sidebar.codemap_progress.no_row_churn codemap_progress,row_identity,throttle,global_disable,combine_publisher state_observation root_swiftpm 4 Throttled activity updates publish per-root progress states without changing rootRows value/identity, and toggling global disable overrides then restores activity-derived states. Progress ticks could rebuild sidebar rows and cause reorder/flicker, or a disable toggle could strand stale progress states. direct_in_process;combine_publisher test_case retain 0 Per-root codemap sidebar indicator: live store churn and disable round-trip.
root/RepoPromptTests.WorkspaceFileContextStoreCodemapSeamTests/testAcceptedProjectionSnapshotsYieldRootProgressStreamEvents root Tests/RepoPromptTests/WorkspaceContext/WorkspaceFileContextStoreCodemapSeamTests.swift RepoPromptTests.WorkspaceFileContextStoreCodemapSeamTests testAcceptedProjectionSnapshotsYieldRootProgressStreamEvents WorkspaceContext/CodeMap codemap.root_progress_stream.accepted_snapshot_events codemap_progress,projection_segment,coverage_seal,async_stream durable_publication_contract root_swiftpm 3 CodemapStoreFixture,ReviewGitRepositoryFixture Accepted projection segments yield non-sealed root progress events with processed/total counts and the accepted seal yields a sealed complete-phase event, all keyed to the loaded root UUID. The sidebar progress stream could miss accepted publications, mis-key roots, or never observe completion. filesystem,git_fixture,artifact_store,actor,concurrency store_session+progress_observer+root_unload retain 0 Per-root codemap sidebar indicator: store progress stream propagation.
root/RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests/testCodemapProgressInitialGlobalDisableSuppressesActivitySynchronously root Tests/RepoPromptTests/AgentMode/AgentWorkspaceRootsSidebarStoreTests.swift RepoPromptTests.AgentWorkspaceRootsSidebarStoreTests testCodemapProgressInitialGlobalDisableSuppressesActivitySynchronously AgentMode agent_sidebar.codemap_progress.initial_global_disable codemap_progress,global_disable,initial_state deterministic_unit root_swiftpm 2 A store initialized while global code maps are disabled publishes disabled states synchronously instead of briefly exposing scan activity. The sidebar could flash active codemap progress during Agent Mode initialization even though the global code-map kill-switch is on. direct_in_process test_case retain 0 Per-root codemap sidebar indicator: initial global disable suppression.
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
import Combine
import Foundation

/// Subtle per-root codemap scanning/loading state rendered under the root
/// identity line in the Agent Mode workspace roots sidebar.
enum AgentRootCodemapProgressDisplayState: Equatable {
/// Projection scan in flight; determinate when `total` is known.
case scanning(processed: UInt64, total: UInt64?)
/// Projection coverage complete for the root's current generation.
case ready
/// The global code maps kill-switch is on; overrides any activity.
case disabledGlobally

var displayText: String {
switch self {
case let .scanning(processed, total?):
"Codemaps \(processed)/\(total)"
case .scanning:
"Codemaps scanning…"
case .ready:
"Codemaps ready"
case .disabledGlobally:
"Codemaps disabled globally"
}
}

var accessibilityText: String {
switch self {
case let .scanning(processed, total?):
"Codemaps scanning, \(processed) of \(total) files processed"
case let .scanning(processed, nil):
"Codemaps scanning, \(processed) files processed"
case .ready:
"Codemaps ready"
case .disabledGlobally:
"Codemaps disabled globally"
}
}
}

struct AgentWorkspaceRootRow: Identifiable, Equatable {
let id: UUID
let name: String
Expand Down Expand Up @@ -54,17 +91,26 @@ final class AgentWorkspaceRootsSidebarStore: ObservableObject {
@Published private(set) var rootRows: [AgentWorkspaceRootRow] = []
@Published private(set) var workspaceLabel = "No Workspace"
@Published private(set) var isExitDisabled = true
/// Per-root codemap progress display states keyed by root UUID
/// (`AgentWorkspaceRootRow.id`). Published separately from `rootRows` so
/// progress ticks never rebuild rows or disturb row identity/ordering.
@Published private(set) var codemapProgressByRootID: [UUID: AgentRootCodemapProgressDisplayState] = [:]

private let rootProjections: @MainActor () -> [WorkspaceRootShellProjection]
private let rootChanges: AnyPublisher<Void, Never>
private let gitContextLookup: @MainActor (String) -> GitWorktreeContextSummary?
private let gitContextChanges: AnyPublisher<Void, Never>
private let codemapActivityLookup: @MainActor () -> [UUID: WorkspaceRootCodemapActivity]
private let codemapActivityChanges: AnyPublisher<Void, Never>
private let codemapsGloballyDisabled: AnyPublisher<Bool, Never>
private let codemapActivityThrottleMilliseconds: Int
private let workspaceManager: WorkspaceManagerViewModel
let windowID: Int

private var cancellables: Set<AnyCancellable> = []
private var rootRowsResnapshotTask: Task<Void, Never>?
private var workspaceMetadataResnapshotTask: Task<Void, Never>?
private var isCodemapsGloballyDisabled = false

var workspaceManagerForPicker: WorkspaceManagerViewModel {
workspaceManager
Expand All @@ -75,13 +121,25 @@ final class AgentWorkspaceRootsSidebarStore: ObservableObject {
rootChanges: AnyPublisher<Void, Never>,
gitContextLookup: @escaping @MainActor (String) -> GitWorktreeContextSummary? = { _ in nil },
gitContextChanges: AnyPublisher<Void, Never> = Empty<Void, Never>().eraseToAnyPublisher(),
codemapActivityLookup: @escaping @MainActor () -> [UUID: WorkspaceRootCodemapActivity] = { [:] },
codemapActivityChanges: AnyPublisher<Void, Never> = Empty<Void, Never>().eraseToAnyPublisher(),
codemapsGloballyDisabled: AnyPublisher<Bool, Never>? = nil,
initialCodemapsGloballyDisabled: Bool? = nil,
codemapActivityThrottleMilliseconds: Int = 150,
workspaceManager: WorkspaceManagerViewModel,
windowID: Int
) {
self.rootProjections = rootProjections
self.rootChanges = rootChanges
self.gitContextLookup = gitContextLookup
self.gitContextChanges = gitContextChanges
self.codemapActivityLookup = codemapActivityLookup
self.codemapActivityChanges = codemapActivityChanges
self.codemapsGloballyDisabled = codemapsGloballyDisabled
?? GlobalSettingsStore.shared.$codeMapsGloballyDisabled.eraseToAnyPublisher()
isCodemapsGloballyDisabled = initialCodemapsGloballyDisabled
?? (codemapsGloballyDisabled == nil ? GlobalSettingsStore.shared.globalCodeMapsDisabled() : false)
self.codemapActivityThrottleMilliseconds = max(0, codemapActivityThrottleMilliseconds)
self.workspaceManager = workspaceManager
self.windowID = windowID

Expand All @@ -95,6 +153,32 @@ final class AgentWorkspaceRootsSidebarStore: ObservableObject {
workspaceMetadataResnapshotTask?.cancel()
}

/// Pure mapping from visible root projections + per-root activity to
/// display states. Progress is joined strictly by root UUID; activity
/// entries whose IDs do not match a visible projection (for example hidden
/// physical worktree session roots) are dropped. Global disable wins.
static func codemapProgressStates(
for projections: [WorkspaceRootShellProjection],
activityByRootID: [UUID: WorkspaceRootCodemapActivity],
globallyDisabled: Bool
) -> [UUID: AgentRootCodemapProgressDisplayState] {
if globallyDisabled {
return Dictionary(uniqueKeysWithValues: projections.map { ($0.id, .disabledGlobally) })
}
var states: [UUID: AgentRootCodemapProgressDisplayState] = [:]
for projection in projections {
switch activityByRootID[projection.id] {
case let .scanning(processed, total):
states[projection.id] = .scanning(processed: processed, total: total)
case .ready:
states[projection.id] = .ready
case nil:
break
}
}
return states
}

static func rows(
from projections: [WorkspaceRootShellProjection],
gitContextLookup: (String) -> GitWorktreeContextSummary? = { _ in nil }
Expand Down Expand Up @@ -174,6 +258,30 @@ final class AgentWorkspaceRootsSidebarStore: ObservableObject {
}
.store(in: &cancellables)

codemapActivityChanges
.throttle(
for: .milliseconds(codemapActivityThrottleMilliseconds),
scheduler: DispatchQueue.main,
latest: true
)
.sink { [weak self] in
Task { @MainActor in
self?.resnapshotCodemapProgress()
}
}
.store(in: &cancellables)

codemapsGloballyDisabled
.removeDuplicates()
.sink { [weak self] disabled in
Task { @MainActor in
guard let self, self.isCodemapsGloballyDisabled != disabled else { return }
self.isCodemapsGloballyDisabled = disabled
self.resnapshotCodemapProgress()
}
}
.store(in: &cancellables)

workspaceManager.objectWillChange
.sink { [weak self] _ in
Task { @MainActor in
Expand Down Expand Up @@ -214,6 +322,18 @@ final class AgentWorkspaceRootsSidebarStore: ObservableObject {
if rootRows != nextRootRows {
rootRows = nextRootRows
}
resnapshotCodemapProgress()
}

private func resnapshotCodemapProgress() {
let next = Self.codemapProgressStates(
for: rootProjections(),
activityByRootID: codemapActivityLookup(),
globallyDisabled: isCodemapsGloballyDisabled
)
if codemapProgressByRootID != next {
codemapProgressByRootID = next
}
}

private func resnapshotWorkspaceMetadata() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ struct AgentModeView: View {
rootChanges: windowState.workspaceFilesViewModel.rootShellProjectionsChangedPublisher,
gitContextLookup: { promptManager.gitViewModel.gitWorktreeContext(forStandardizedRootPath: $0) },
gitContextChanges: promptManager.gitViewModel.gitWorktreeContextChanges,
codemapActivityLookup: { windowState.workspaceFilesViewModel.codemapRootActivityByRootID },
codemapActivityChanges: windowState.workspaceFilesViewModel.codemapRootActivityChangedPublisher,
workspaceManager: windowState.workspaceManager,
windowID: windowState.windowID
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ struct AgentWorkspaceRootsSectionView: View {

private func rowHasContextLine(_ row: AgentWorkspaceRootRow) -> Bool {
row.gitContext != nil || row.worktree != nil || mergeAttention(for: row) != nil
|| codemapProgress(for: row) != nil
}

/// Resolves the subtle per-root codemap scanning/loading state for `row`
/// by root UUID. Absent (idle) roots render nothing.
private func codemapProgress(for row: AgentWorkspaceRootRow) -> AgentRootCodemapProgressDisplayState? {
rootsStore.codemapProgressByRootID[row.id]
}

private var shouldScrollFolderList: Bool {
Expand Down Expand Up @@ -477,10 +484,37 @@ struct AgentWorkspaceRootsSectionView: View {
.layoutPriority(0)
}

if let codemapState = codemapProgress(for: row) {
codemapProgressLabel(codemapState)
.layoutPriority(-1)
}

Spacer(minLength: 0)
}
}

// MARK: - Codemap Progress Label

/// Subtle per-root codemap scanning/loading indicator rendered under the
/// root identity line. Deliberately quiet: small secondary text, no
/// spinner, truncates before capsules at narrow widths.
private func codemapProgressLabel(_ state: AgentRootCodemapProgressDisplayState) -> some View {
Text(state.displayText)
.font(fontPreset.swiftUIFont(sizeAtNormal: 9))
.foregroundColor(.secondary)
.opacity(codemapProgressOpacity(state))
.lineLimit(1)
.truncationMode(.tail)
.accessibilityLabel(state.accessibilityText)
}

private func codemapProgressOpacity(_ state: AgentRootCodemapProgressDisplayState) -> Double {
switch state {
case .scanning: 0.9
case .ready, .disabledGlobally: 0.55
}
}

private func rootActionsOverlay(_ row: AgentWorkspaceRootRow, hasMultipleRoots: Bool) -> some View {
HStack(spacing: rootActionOverlaySpacing) {
if hasMultipleRoots {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

/// Compact per-root codemap projection activity derived from accepted
/// `WorkspaceCodemapRootProjectionProgressEvent`s. Keyed by root UUID
/// (`FolderViewModel.id` / `WorkspaceRootShellProjection.id`); never keyed by path.
enum WorkspaceRootCodemapActivity: Equatable {
/// Projection scan/publish in flight. `total` is known only once the
/// projection catalog has completed paging; a nil total renders as
/// indeterminate progress.
case scanning(processed: UInt64, total: UInt64?)
/// Projection coverage is sealed/complete for the root's current generation.
case ready(processed: UInt64)

init(event: WorkspaceCodemapRootProjectionProgressEvent) {
let processed = event.progress.counts.processedCandidateCount
if event.isSealed || event.progress.phase == .complete {
self = .ready(processed: processed)
} else {
self = .scanning(
processed: processed,
total: event.progress.catalogCompletion?.supportedCandidateCount
)
}
}
}
Loading
Loading