From 40549d8b4f0f66b78799901135113d18087a402a Mon Sep 17 00:00:00 2001 From: arzafran Date: Wed, 7 Oct 2026 17:46:48 -0300 Subject: [PATCH 1/2] fix: keep the sidebar agent badge readable and drop spinner glyphs from titles --- Sources/TabItemView.swift | 30 ++++++++++++++++++++- programaTests/ProgramStatusStoreTests.swift | 11 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Sources/TabItemView.swift b/Sources/TabItemView.swift index d15e240ee..3311745d5 100644 --- a/Sources/TabItemView.swift +++ b/Sources/TabItemView.swift @@ -65,6 +65,25 @@ struct SidebarRowMetrics: Equatable { } } +enum SidebarTitle { + /// Drops one leading status glyph plus its following space, e.g. the spinner Claude Code + /// puts in its terminal title ("◐ Fix tests", "✳ Claude Code", "⠋ Build"). Only a text + /// symbol counts: letters, digits, punctuation and emoji presented as emoji stay, so + /// "🚀 Deploy" is left alone. + static func strippingLeadingStatusGlyph(_ title: String) -> String { + guard let first = title.unicodeScalars.first, + first.properties.generalCategory == .otherSymbol, + !first.properties.isEmojiPresentation, + 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 +295,12 @@ struct TabItemView: View, Equatable { SidebarAgentIndicator.make(for: tab) } + /// Program-set titles lose a leading spinner glyph (the agent badge already shows that + /// state); a title the user typed is shown exactly as written. + private var sidebarDisplayTitle: String { + tab.customTitle == nil ? SidebarTitle.strippingLeadingStatusGlyph(tab.title) : 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 +459,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 +481,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..9fd65ed2f 100644 --- a/programaTests/ProgramStatusStoreTests.swift +++ b/programaTests/ProgramStatusStoreTests.swift @@ -232,4 +232,15 @@ 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("◐ "), "◐ ") + } } From 048d1528d555a694595e699fcc60dde028352114 Mon Sep 17 00:00:00 2001 From: arzafran Date: Wed, 7 Oct 2026 17:49:07 -0300 Subject: [PATCH 2/2] fix: strip only known spinner glyphs, and only while the agent badge shows --- Sources/TabItemView.swift | 30 ++++++++++++++------- programaTests/ProgramStatusStoreTests.swift | 2 ++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Sources/TabItemView.swift b/Sources/TabItemView.swift index 3311745d5..0b9b6bf07 100644 --- a/Sources/TabItemView.swift +++ b/Sources/TabItemView.swift @@ -66,14 +66,24 @@ struct SidebarRowMetrics: Equatable { } enum SidebarTitle { - /// Drops one leading status glyph plus its following space, e.g. the spinner Claude Code - /// puts in its terminal title ("◐ Fix tests", "✳ Claude Code", "⠋ Build"). Only a text - /// symbol counts: letters, digits, punctuation and emoji presented as emoji stay, so - /// "🚀 Deploy" is left alone. + /// 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, - first.properties.generalCategory == .otherSymbol, - !first.properties.isEmojiPresentation, + isStatusGlyph(first), let firstCharacter = title.first, firstCharacter.unicodeScalars.count == 1 || firstCharacter.unicodeScalars.dropFirst().allSatisfy({ $0.value == 0xFE0E }), title.dropFirst().first == " " else { @@ -295,10 +305,12 @@ struct TabItemView: View, Equatable { SidebarAgentIndicator.make(for: tab) } - /// Program-set titles lose a leading spinner glyph (the agent badge already shows that - /// state); a title the user typed is shown exactly as written. + /// 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 { - tab.customTitle == nil ? SidebarTitle.strippingLeadingStatusGlyph(tab.title) : tab.title + 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. diff --git a/programaTests/ProgramStatusStoreTests.swift b/programaTests/ProgramStatusStoreTests.swift index 9fd65ed2f..a5cd0e2a2 100644 --- a/programaTests/ProgramStatusStoreTests.swift +++ b/programaTests/ProgramStatusStoreTests.swift @@ -242,5 +242,7 @@ final class ProgramStatusStoreTests: XCTestCase { XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("main"), "main") XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("◐"), "◐") XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("◐ "), "◐ ") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("© Example"), "© Example") + XCTAssertEqual(SidebarTitle.strippingLeadingStatusGlyph("™ Product"), "™ Product") } }