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
1 change: 1 addition & 0 deletions Sources/Workspace+Bonsplit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ extension Workspace: @preconcurrency BonsplitDelegate {
reviewNudgeProbeGenerations.removeValue(forKey: panelId)
reviewNudgeBaselines.removeValue(forKey: panelId)
panelTitles.removeValue(forKey: panelId)
panelsHoldingRestoredTitle.remove(panelId)
panelCustomTitles.removeValue(forKey: panelId)
pinnedPanelIds.remove(panelId)
manualUnreadPanelIds.remove(panelId)
Expand Down
4 changes: 4 additions & 0 deletions Sources/Workspace+Persistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,10 @@ extension Workspace {
if let title = snapshot.title?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
panelTitles[panelId] = title
panelsWithLiveTitle.insert(panelId)
if !Self.isTerminalStartupTitle(title) {
panelsHoldingRestoredTitle.insert(panelId)
}
syncResolvedPanelTitle(panelId: panelId)
}

setPanelCustomTitle(panelId: panelId, title: snapshot.customTitle)
Expand Down
21 changes: 21 additions & 0 deletions Sources/Workspace+SidebarTelemetry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,10 @@ extension Workspace {
func updatePanelTitle(panelId: UUID, title: String) -> Bool {
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
if panelsHoldingRestoredTitle.contains(panelId) {
if Self.isTerminalStartupTitle(trimmed) { return false }
panelsHoldingRestoredTitle.remove(panelId)
}
var didMutate = false

if panelTitles[panelId] != trimmed {
Expand All @@ -522,6 +526,22 @@ extension Workspace {
return didMutate
}

/// Titles a terminal shows before it has anything of its own to say: a shell's cwd
/// (`/path`, `~/path`, `user@host: ~/path`), the `AgentResumeCommand` line being run, and
/// an agent's banner before its session has a topic.
static func isTerminalStartupTitle(_ title: String) -> Bool {
var text = title.trimmingCharacters(in: .whitespacesAndNewlines)
if let range = text.range(of: #"^[^\s@:]+@[^\s:]+:\s*"#, options: .regularExpression) {
text.removeSubrange(range)
}
if text.hasPrefix("/") || text.hasPrefix("~") { return true }
if text.range(of: #"(^|&& )(claude --resume|codex resume) "#, options: .regularExpression) != nil {
return true
}
let banner = text.drop { !$0.isLetter && !$0.isNumber }
return banner.caseInsensitiveCompare("Claude Code") == .orderedSame
}

/// Re-derive the workspace title from the focused panel's last known
/// title. Called on pane-focus changes so the sidebar follows the active
/// pane without waiting for it to emit a new OSC title. Only titles that
Expand All @@ -544,6 +564,7 @@ extension Workspace {
}
if panelsWithLiveTitle.contains(where: { !validSurfaceIds.contains($0) }) {
panelsWithLiveTitle = panelsWithLiveTitle.filter { validSurfaceIds.contains($0) }
panelsHoldingRestoredTitle = panelsHoldingRestoredTitle.filter { validSurfaceIds.contains($0) }
}
if panelCustomTitles.keys.contains(where: { !validSurfaceIds.contains($0) }) {
panelCustomTitles = panelCustomTitles.filter { validSurfaceIds.contains($0.key) }
Expand Down
4 changes: 4 additions & 0 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ final class Workspace: Identifiable, ObservableObject {
/// session restore), as opposed to the displayTitle placeholder seeded at
/// panel creation. Only these titles may be promoted to the workspace title.
var panelsWithLiveTitle: Set<UUID> = []
/// Panels whose title came from session restore and has not yet been replaced by a real
/// title. While held, the restored shell's startup titles (its cwd, a typed resume
/// command, an agent's untitled banner) are ignored so the saved title stays visible.
var panelsHoldingRestoredTitle: Set<UUID> = []
@Published var panelCustomTitles: [UUID: String] = [:]
@Published var pinnedPanelIds: Set<UUID> = []
/// Per-tab color tint (pane tab bar), keyed by panel id. Distinct from `customColor`,
Expand Down
51 changes: 51 additions & 0 deletions programaTests/WorkspaceUnitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3170,6 +3170,57 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
assertPanelAndTabTitle("live-process", workspace: restored, panelId: restoredPanelId)
}

func testSessionRestoreKeepsTitleUntilTheShellSendsARealOne() throws {
let source = Workspace()
let restored = Workspace()
defer {
source.teardownAllPanels()
restored.teardownAllPanels()
}
let panelId = try XCTUnwrap(source.focusedPanelId)
XCTAssertTrue(source.updatePanelTitle(panelId: panelId, title: "Reviewing drive capital proposal"))

restored.restoreSessionSnapshot(source.sessionSnapshot(includeScrollback: false))
let restoredPanelId = try XCTUnwrap(restored.focusedPanelId)

for placeholder in [
"/Users/someone/gitlab/project",
"~/gitlab/project",
"someone@host: ~/gitlab/project",
"cd -- '/Users/someone/gitlab/project' && claude --resume 3a8f89e1",
"claude --resume 3a8f89e1",
"codex resume 3a8f89e1",
"Claude Code",
"✳ Claude Code",
] {
restored.updatePanelTitle(panelId: restoredPanelId, title: placeholder)
assertPanelAndTabTitle("Reviewing drive capital proposal", workspace: restored, panelId: restoredPanelId)
}

XCTAssertTrue(restored.updatePanelTitle(panelId: restoredPanelId, title: "Fixing the tab strip"))
assertPanelAndTabTitle("Fixing the tab strip", workspace: restored, panelId: restoredPanelId)

XCTAssertTrue(restored.updatePanelTitle(panelId: restoredPanelId, title: "~/gitlab/project"))
assertPanelAndTabTitle("~/gitlab/project", workspace: restored, panelId: restoredPanelId)
}

func testSessionRestoredPathTitleYieldsToTheShell() throws {
let source = Workspace()
let restored = Workspace()
defer {
source.teardownAllPanels()
restored.teardownAllPanels()
}
let panelId = try XCTUnwrap(source.focusedPanelId)
XCTAssertTrue(source.updatePanelTitle(panelId: panelId, title: "~/gitlab/old"))

restored.restoreSessionSnapshot(source.sessionSnapshot(includeScrollback: false))
let restoredPanelId = try XCTUnwrap(restored.focusedPanelId)

XCTAssertTrue(restored.updatePanelTitle(panelId: restoredPanelId, title: "~/gitlab/new"))
assertPanelAndTabTitle("~/gitlab/new", workspace: restored, panelId: restoredPanelId)
}

func testTerminalSplitWithFocusFalsePreservesOriginalFocusedPanel() {
let workspace = Workspace()
guard let originalFocusedPanelId = workspace.focusedPanelId else {
Expand Down
Loading