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
42 changes: 41 additions & 1 deletion Sources/TabItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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))
}
Expand Down
13 changes: 13 additions & 0 deletions programaTests/ProgramStatusStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Loading