From 1ecb41f437f96694f2464f81d286d79390d5c4c2 Mon Sep 17 00:00:00 2001 From: Eric Provencher Date: Fri, 19 Jun 2026 15:35:04 -0400 Subject: [PATCH] Clarify sliced file selection summaries --- .../Services/AgentContextExportResolver.swift | 43 ++++++-- .../Views/Components/AgentContextPill.swift | 14 +-- .../AgentRuntimeExportCard.swift | 15 +-- .../AgentContextExportResolverTests.swift | 100 ++++++++++++++++++ 4 files changed, 151 insertions(+), 21 deletions(-) diff --git a/Sources/RepoPrompt/Features/AgentMode/Services/AgentContextExportResolver.swift b/Sources/RepoPrompt/Features/AgentMode/Services/AgentContextExportResolver.swift index bf9b1e443..0e508d2a7 100644 --- a/Sources/RepoPrompt/Features/AgentMode/Services/AgentContextExportResolver.swift +++ b/Sources/RepoPrompt/Features/AgentMode/Services/AgentContextExportResolver.swift @@ -34,6 +34,20 @@ struct AgentContextExportIdentity: Equatable { let worktreeBindingFingerprint: String } +struct AgentContextSelectionSummary: Equatable { + let totalExplicitFileCount: Int + let fullFileCount: Int + let slicedFileCount: Int + let sliceRangeCount: Int + + var headlineText: String { + let fileText = "\(totalExplicitFileCount) file\(totalExplicitFileCount == 1 ? "" : "s")" + guard slicedFileCount > 0 else { return fileText } + let rangeText = "\(sliceRangeCount) range\(sliceRangeCount == 1 ? "" : "s")" + return "\(fileText) · \(slicedFileCount) sliced · \(rangeText)" + } +} + struct AgentContextExportSourceBuildRequest { let requestedTabID: UUID? let activeComposeTabID: UUID? @@ -167,22 +181,35 @@ enum AgentContextExportResolver { let canRemove: Bool } - static func explicitSelectionFileCount(_ selection: StoredSelection) -> Int { - var seen = Set() - for path in selection.selectedPaths { - seen.insert(normalizedSelectionKey(path)) - } + static func selectionSummary(for selection: StoredSelection) -> AgentContextSelectionSummary { + var explicitFileKeys = Set(selection.selectedPaths.map(normalizedSelectionKey)) + var slicedFileKeys = Set() + var sliceRangeCount = 0 + for (path, ranges) in selection.slices where !ranges.isEmpty { - seen.insert(normalizedSelectionKey(path)) + let key = normalizedSelectionKey(path) + explicitFileKeys.insert(key) + slicedFileKeys.insert(key) + sliceRangeCount += ranges.count } - return seen.count + + return AgentContextSelectionSummary( + totalExplicitFileCount: explicitFileKeys.count, + fullFileCount: explicitFileKeys.count - slicedFileKeys.count, + slicedFileCount: slicedFileKeys.count, + sliceRangeCount: sliceRangeCount + ) + } + + static func explicitSelectionFileCount(_ selection: StoredSelection) -> Int { + selectionSummary(for: selection).totalExplicitFileCount } static func displayFileCount( resolvedModel _: AgentContextExportModel?, sourceSelection: StoredSelection ) -> Int { - explicitSelectionFileCount(sourceSelection) + selectionSummary(for: sourceSelection).totalExplicitFileCount } static func lookupContext( diff --git a/Sources/RepoPrompt/Features/AgentMode/Views/Components/AgentContextPill.swift b/Sources/RepoPrompt/Features/AgentMode/Views/Components/AgentContextPill.swift index 4e9bd1455..395fd1f1f 100644 --- a/Sources/RepoPrompt/Features/AgentMode/Views/Components/AgentContextPill.swift +++ b/Sources/RepoPrompt/Features/AgentMode/Views/Components/AgentContextPill.swift @@ -26,11 +26,12 @@ struct AgentContextPill: View { runtimeVM.snapshot.effectiveContextWindowTokens } + private var selectionSummary: AgentContextSelectionSummary { + AgentContextExportResolver.selectionSummary(for: currentExportSourceSelection) + } + private var fileCount: Int { - AgentContextExportResolver.displayFileCount( - resolvedModel: nil, - sourceSelection: currentExportSourceSelection - ) + selectionSummary.totalExplicitFileCount } private var currentExportSourceSelection: StoredSelection { @@ -56,7 +57,7 @@ struct AgentContextPill: View { } private var fileSummaryText: String { - "\(fileCount) file\(fileCount == 1 ? "" : "s")" + selectionSummary.headlineText } private var contextUsageTooltip: String { @@ -95,6 +96,7 @@ struct AgentContextPill: View { .font(fontPreset.swiftUIFont(sizeAtNormal: 12, weight: .medium)) .foregroundStyle(.secondary) .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) AgentContextIndicator( contextWindowTokens: contextWindowTokens, @@ -138,7 +140,7 @@ struct AgentContextPill: View { Text("Selected") .font(fontPreset.swiftUIFont(sizeAtNormal: 10)) .foregroundStyle(.tertiary) - Text("\(fileCount) files") + Text(fileSummaryText) .font(fontPreset.swiftUIFont(sizeAtNormal: 12, weight: .semibold)) } } diff --git a/Sources/RepoPrompt/Features/AgentMode/Views/RuntimeSidebar/AgentRuntimeExportCard.swift b/Sources/RepoPrompt/Features/AgentMode/Views/RuntimeSidebar/AgentRuntimeExportCard.swift index ace215713..d0277006f 100644 --- a/Sources/RepoPrompt/Features/AgentMode/Views/RuntimeSidebar/AgentRuntimeExportCard.swift +++ b/Sources/RepoPrompt/Features/AgentMode/Views/RuntimeSidebar/AgentRuntimeExportCard.swift @@ -26,10 +26,9 @@ struct AgentExportCard: View { makeExportSource(flushPendingUI: false).exportContextIdentity } - private var displayFileCount: Int { - AgentContextExportResolver.displayFileCount( - resolvedModel: currentExportModel, - sourceSelection: makeExportSource(flushPendingUI: false).selection + private var selectionSummary: AgentContextSelectionSummary { + AgentContextExportResolver.selectionSummary( + for: makeExportSource(flushPendingUI: false).selection ) } @@ -103,7 +102,8 @@ struct AgentExportCard: View { // MARK: - Files Button private var filesButton: some View { - let selectionCount = displayFileCount + let summary = selectionSummary + let selectionCount = summary.totalExplicitFileCount return Button { showSelectedFilesPopover.toggle() @@ -112,9 +112,10 @@ struct AgentExportCard: View { Image(systemName: "doc.on.doc") .font(.system(size: 10, weight: .medium)) .foregroundStyle(.secondary) - Text("\(selectionCount) file\(selectionCount == 1 ? "" : "s")") + Text(summary.headlineText) .font(.system(size: 10, weight: .medium)) - .lineLimit(1) + .multilineTextAlignment(.trailing) + .lineLimit(2) } .padding(.horizontal, 8) .padding(.vertical, 5) diff --git a/Tests/RepoPromptTests/AgentMode/AgentContextExportResolverTests.swift b/Tests/RepoPromptTests/AgentMode/AgentContextExportResolverTests.swift index 45bb2c41e..d4005dd7b 100644 --- a/Tests/RepoPromptTests/AgentMode/AgentContextExportResolverTests.swift +++ b/Tests/RepoPromptTests/AgentMode/AgentContextExportResolverTests.swift @@ -30,6 +30,106 @@ final class AgentContextExportResolverTests: XCTestCase { ) } + func testSelectionSummaryDistinguishesFullAndSlicedFiles() { + let selection = StoredSelection( + selectedPaths: ["Sources/Full.swift", "Sources/Sliced.swift"], + slices: [ + "Sources/Sliced.swift": [ + LineRange(start: 2, end: 4), + LineRange(start: 8, end: 10) + ] + ], + codemapAutoEnabled: false + ) + + let summary = AgentContextExportResolver.selectionSummary(for: selection) + + XCTAssertEqual(summary.totalExplicitFileCount, 2) + XCTAssertEqual(summary.fullFileCount, 1) + XCTAssertEqual(summary.slicedFileCount, 1) + XCTAssertEqual(summary.sliceRangeCount, 2) + XCTAssertEqual(summary.headlineText, "2 files · 1 sliced · 2 ranges") + } + + func testSelectionSummaryIncludesLegacySliceOnlyKey() { + let selection = StoredSelection( + slices: ["Sources/SliceOnly.swift": [LineRange(start: 3, end: 7)]], + codemapAutoEnabled: false + ) + + let summary = AgentContextExportResolver.selectionSummary(for: selection) + + XCTAssertEqual(summary.totalExplicitFileCount, 1) + XCTAssertEqual(summary.fullFileCount, 0) + XCTAssertEqual(summary.slicedFileCount, 1) + XCTAssertEqual(summary.sliceRangeCount, 1) + XCTAssertEqual(summary.headlineText, "1 file · 1 sliced · 1 range") + } + + func testSelectionSummaryDeduplicatesSelectedPathWithSlices() { + let selection = StoredSelection( + selectedPaths: ["Sources/App.swift"], + slices: ["Sources/App.swift": [LineRange(start: 1, end: 2)]], + codemapAutoEnabled: false + ) + + let summary = AgentContextExportResolver.selectionSummary(for: selection) + + XCTAssertEqual(summary.totalExplicitFileCount, 1) + XCTAssertEqual(summary.fullFileCount, 0) + XCTAssertEqual(summary.slicedFileCount, 1) + XCTAssertEqual(summary.sliceRangeCount, 1) + } + + func testSelectionSummaryExcludesEmptySlicesAndAutoCodemaps() { + let selection = StoredSelection( + autoCodemapPaths: ["Sources/Dependency.swift"], + slices: ["Sources/Empty.swift": []], + codemapAutoEnabled: true + ) + + let summary = AgentContextExportResolver.selectionSummary(for: selection) + + XCTAssertEqual(summary.totalExplicitFileCount, 0) + XCTAssertEqual(summary.fullFileCount, 0) + XCTAssertEqual(summary.slicedFileCount, 0) + XCTAssertEqual(summary.sliceRangeCount, 0) + XCTAssertEqual(summary.headlineText, "0 files") + } + + func testSelectionSummaryRetainsFullOnlyFormatting() { + let singular = AgentContextExportResolver.selectionSummary( + for: StoredSelection(selectedPaths: ["One.swift"], codemapAutoEnabled: false) + ) + let plural = AgentContextExportResolver.selectionSummary( + for: StoredSelection(selectedPaths: ["One.swift", "Two.swift"], codemapAutoEnabled: false) + ) + + XCTAssertEqual(singular.headlineText, "1 file") + XCTAssertEqual(plural.headlineText, "2 files") + } + + func testSelectionSummaryDeduplicatesNormalizedAliasesAndSumsStoredRanges() { + let selection = StoredSelection( + slices: [ + "Sources/Alias.swift": [LineRange(start: 1, end: 2)], + " Sources/Alias.swift ": [ + LineRange(start: 4, end: 5), + LineRange(start: 8, end: 9) + ] + ], + codemapAutoEnabled: false + ) + + let summary = AgentContextExportResolver.selectionSummary(for: selection) + + XCTAssertEqual(summary.totalExplicitFileCount, 1) + XCTAssertEqual(summary.fullFileCount, 0) + XCTAssertEqual(summary.slicedFileCount, 1) + XCTAssertEqual(summary.sliceRangeCount, 3) + XCTAssertEqual(summary.headlineText, "1 file · 1 sliced · 3 ranges") + } + func testAutoCodemapExportResolutionBatchesPopoverPathLookups() async throws { #if DEBUG let root = try makeTemporaryRoot(name: "AgentExportAutoCodemapBatch")