diff --git a/Sources/TabItemView.swift b/Sources/TabItemView.swift index d15e240ee..0b9b6bf07 100644 --- a/Sources/TabItemView.swift +++ b/Sources/TabItemView.swift @@ -65,6 +65,35 @@ struct SidebarRowMetrics: Equatable { } } +enum SidebarTitle { + /// Spinner frames agents put at the start of their terminal title: braille spinners, + /// quarter-circle spinners, and Claude Code's star frames. An explicit set, so ordinary + /// symbols such as "©" or "™" are never stripped. + private static func isStatusGlyph(_ scalar: Unicode.Scalar) -> Bool { + switch scalar.value { + case 0x2801...0x28FF, 0x25D0...0x25D3, 0x25F4...0x25F7, + 0x00B7, 0x2722, 0x2733, 0x2736, 0x273B, 0x273D: + return true + default: + return false + } + } + + /// Drops one leading spinner glyph plus its following space ("◐ Fix tests" becomes + /// "Fix tests"). A title made of the glyph alone is left as is. + static func strippingLeadingStatusGlyph(_ title: String) -> String { + guard let first = title.unicodeScalars.first, + isStatusGlyph(first), + let firstCharacter = title.first, + firstCharacter.unicodeScalars.count == 1 || firstCharacter.unicodeScalars.dropFirst().allSatisfy({ $0.value == 0xFE0E }), + title.dropFirst().first == " " else { + return title + } + let rest = title.dropFirst(2) + return rest.isEmpty ? title : String(rest) + } +} + private enum SidebarWorktreeCreationResult: Sendable { case success(path: String, branch: String) case branchCheckedOut(path: String) @@ -276,6 +305,14 @@ struct TabItemView: View, Equatable { SidebarAgentIndicator.make(for: tab) } + /// Program-set titles lose a leading spinner glyph only while the agent badge shows that + /// state; without a badge the glyph is the row's only activity sign. A title the user + /// typed is shown exactly as written. + private var sidebarDisplayTitle: String { + guard tab.customTitle == nil, agentIndicator != nil else { return tab.title } + return SidebarTitle.strippingLeadingStatusGlyph(tab.title) + } + /// Status entry keys/values that used to duplicate agent state in the metadata rows. /// Filtered out at render time so older CLI builds that still send them don't double up /// with the badge above. @@ -434,7 +471,7 @@ struct TabItemView: View, Equatable { } .frame(minWidth: SidebarRowMetrics.leadingIconSlotWidth, alignment: .center) - Text(tab.title) + Text(sidebarDisplayTitle) .font(.system(size: rowMetrics.titleFontSize, weight: titleFontWeight)) .foregroundColor(activePrimaryTextColor) .lineLimit(1) @@ -456,6 +493,9 @@ struct TabItemView: View, Equatable { .opacity(agentIndicator.isStale ? 0.55 : 1) .lineLimit(1) } + // The badge keeps its full width; the title truncates first. + .fixedSize(horizontal: true, vertical: false) + .layoutPriority(2) .safeHelp(agentIndicator.label) .accessibilityLabel(Text(agentIndicator.label)) } diff --git a/programaTests/ProgramStatusStoreTests.swift b/programaTests/ProgramStatusStoreTests.swift index 6d7157036..a5cd0e2a2 100644 --- a/programaTests/ProgramStatusStoreTests.swift +++ b/programaTests/ProgramStatusStoreTests.swift @@ -232,4 +232,17 @@ final class ProgramStatusStoreTests: XCTestCase { workspace.updatePanelAgentState(panelId: a, state: .blocked, source: .hooks) XCTAssertEqual(SidebarAgentIndicator.make(for: workspace)?.tint, .blocked) } + + func testSidebarTitleStripsLeadingSpinnerGlyphOnly() { + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("◐ CPU usage monitoring"), "CPU usage monitoring") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("✳ Claude Code"), "Claude Code") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("⠋ Build"), "Build") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("🚀 Deploy"), "🚀 Deploy") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("~/Developer"), "~/Developer") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("main"), "main") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("◐"), "◐") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("◐ "), "◐ ") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("© Example"), "© Example") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("™ Product"), "™ Product") + } }