From d08921de6b6d12c58841d4b624431cc8def95b6a Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 22:37:05 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8=20feat(status-bar):=20add=20Claud?= =?UTF-8?q?e=20Code=20session=20status=20aggregator=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add iTermStatusBarClaudeCodeComponent that displays aggregated session statuses (Waiting/Working/Idle) across all windows with a clickable popover showing session details. Includes ClaudeCodeStatusPopoverViewController for navigation, ClaudeCodeSummaryBuilder for testable logic, and 21 unit tests. --- .../ClaudeCodeSummaryBuilderTests.swift | 138 ++++++++++++++++ ...laudeCodeStatusPopoverViewController.swift | 148 ++++++++++++++++++ .../iTermStatusBarClaudeCodeComponent.swift | 135 ++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100644 ModernTests/ClaudeCodeSummaryBuilderTests.swift create mode 100644 sources/ClaudeCodeStatusPopoverViewController.swift create mode 100644 sources/iTermStatusBarClaudeCodeComponent.swift diff --git a/ModernTests/ClaudeCodeSummaryBuilderTests.swift b/ModernTests/ClaudeCodeSummaryBuilderTests.swift new file mode 100644 index 0000000000..512c858834 --- /dev/null +++ b/ModernTests/ClaudeCodeSummaryBuilderTests.swift @@ -0,0 +1,138 @@ +// +// ClaudeCodeSummaryBuilderTests.swift +// iTerm2 +// + +import XCTest +@testable import iTerm2SharedARC + +final class ClaudeCodeSummaryBuilderTests: XCTestCase { + + // MARK: - Helpers + + private func makeStatus(_ text: String, id: String = UUID().uuidString) -> iTermSessionTabStatus { + let s = iTermSessionTabStatus(sessionID: id) + s.statusText = text + return s + } + + // MARK: - isClaudeCodeStatus + + func testIsClaudeCodeStatus_waiting() { + XCTAssertTrue(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("Waiting")) + } + + func testIsClaudeCodeStatus_working() { + XCTAssertTrue(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("Working\u{2026}")) + } + + func testIsClaudeCodeStatus_idle() { + XCTAssertTrue(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("Idle")) + } + + func testIsClaudeCodeStatus_nil() { + XCTAssertFalse(ClaudeCodeSummaryBuilder.isClaudeCodeStatus(nil)) + } + + func testIsClaudeCodeStatus_emptyString() { + XCTAssertFalse(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("")) + } + + func testIsClaudeCodeStatus_unrelatedStatus() { + XCTAssertFalse(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("Running")) + } + + func testIsClaudeCodeStatus_partialMatch() { + // "Waiting" must match exactly — a prefix should not pass. + XCTAssertFalse(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("Wait")) + } + + func testIsClaudeCodeStatus_wrongEllipsis() { + // Three ASCII dots are not the Unicode ellipsis character used in "Working…". + XCTAssertFalse(ClaudeCodeSummaryBuilder.isClaudeCodeStatus("Working...")) + } + + // MARK: - buildSummary — empty + + func testBuildSummary_empty() { + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: []), "No sessions") + } + + // MARK: - buildSummary — single counts (singular form) + + func testBuildSummary_oneWaiting() { + let sessions = [makeStatus("Waiting")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 waiting") + } + + func testBuildSummary_oneWorking() { + let sessions = [makeStatus("Working\u{2026}")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 working") + } + + func testBuildSummary_oneIdle() { + let sessions = [makeStatus("Idle")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 idle") + } + + // MARK: - buildSummary — plural counts + + func testBuildSummary_twoWaiting() { + let sessions = [makeStatus("Waiting"), makeStatus("Waiting")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "2 waiting") + } + + func testBuildSummary_twoWorking() { + let sessions = [makeStatus("Working\u{2026}"), makeStatus("Working\u{2026}")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "2 working") + } + + func testBuildSummary_twoIdle() { + let sessions = [makeStatus("Idle"), makeStatus("Idle")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "2 idle") + } + + // MARK: - buildSummary — mixed states (ordering: waiting, working, idle) + + func testBuildSummary_waitingAndWorking() { + let sessions = [makeStatus("Waiting"), makeStatus("Working\u{2026}")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 waiting, 1 working") + } + + func testBuildSummary_waitingAndIdle() { + let sessions = [makeStatus("Waiting"), makeStatus("Idle")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 waiting, 1 idle") + } + + func testBuildSummary_workingAndIdle() { + let sessions = [makeStatus("Working\u{2026}"), makeStatus("Idle")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 working, 1 idle") + } + + func testBuildSummary_allThreeStates() { + let sessions = [ + makeStatus("Waiting"), + makeStatus("Working\u{2026}"), + makeStatus("Idle"), + ] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "1 waiting, 1 working, 1 idle") + } + + func testBuildSummary_multipleOfEachState() { + let sessions = [ + makeStatus("Waiting"), makeStatus("Waiting"), + makeStatus("Working\u{2026}"), + makeStatus("Idle"), makeStatus("Idle"), makeStatus("Idle"), + ] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "2 waiting, 1 working, 3 idle") + } + + // MARK: - buildSummary — exemplar from the status bar (regression guard) + + func testBuildSummary_exemplarString() { + // The exemplar shown in statusBarComponentExemplar is "2 waiting, 1 working". + // This test guards against regressions that would silently change the format. + let sessions = [makeStatus("Waiting"), makeStatus("Waiting"), makeStatus("Working\u{2026}")] + XCTAssertEqual(ClaudeCodeSummaryBuilder.buildSummary(from: sessions), "2 waiting, 1 working") + } +} diff --git a/sources/ClaudeCodeStatusPopoverViewController.swift b/sources/ClaudeCodeStatusPopoverViewController.swift new file mode 100644 index 0000000000..c292ef004b --- /dev/null +++ b/sources/ClaudeCodeStatusPopoverViewController.swift @@ -0,0 +1,148 @@ +import AppKit + +class ClaudeCodeStatusPopoverViewController: NSViewController { + private var tableView: NSTableView! + private var sessions: [iTermSessionTabStatus] = [] + + override var preferredContentSize: NSSize { + get { NSSize(width: 320, height: 200) } + set {} + } + + override func loadView() { + view = NSView(frame: NSRect(origin: .zero, size: preferredContentSize)) + view.translatesAutoresizingMaskIntoConstraints = false + + let scrollView = NSScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.hasVerticalScroller = true + scrollView.autohidesScrollers = true + + tableView = NSTableView() + tableView.allowsMultipleSelection = false + tableView.headerView = nil + tableView.backgroundColor = .clear + + let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) + nameColumn.width = 220 + nameColumn.minWidth = 100 + tableView.addTableColumn(nameColumn) + + let statusColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("status")) + statusColumn.width = 84 + statusColumn.minWidth = 60 + tableView.addTableColumn(statusColumn) + + tableView.dataSource = self + tableView.delegate = self + + scrollView.documentView = tableView + view.addSubview(scrollView) + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.widthAnchor.constraint(equalToConstant: preferredContentSize.width), + view.heightAnchor.constraint(equalToConstant: preferredContentSize.height), + ]) + + reloadSessions() + + // TODO: Add Allow/Deny buttons here once a mechanism exists for injecting + // keystrokes into Claude's TUI without ambiguity about which session to target. + } + + private func reloadSessions() { + sessions = claudeCodeSessions() + tableView?.reloadData() + } + + private func claudeCodeSessions() -> [iTermSessionTabStatus] { + let filtered = SessionStatusController.instance.statuses.values.filter { + ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) + } + return filtered.sorted { lhs, rhs in + sortOrder(lhs.statusText) < sortOrder(rhs.statusText) + } + } + + private func sortOrder(_ statusText: String?) -> Int { + switch statusText { + case ClaudeCodeSummaryBuilder.waitingText: return 0 + case ClaudeCodeSummaryBuilder.workingText: return 1 + case ClaudeCodeSummaryBuilder.idleText: return 2 + default: return 3 + } + } + + private func sessionName(for sessionID: String) -> String { + guard let session = iTermController.sharedInstance()?.session(withGUID: sessionID) else { + return sessionID + } + return session.name ?? sessionID + } + + private func dotColor(for status: iTermSessionTabStatus) -> NSColor? { + guard status.hasStatusTextColor else { return nil } + let c = status.statusTextColor + return NSColor(srgbRed: CGFloat(c.r), green: CGFloat(c.g), blue: CGFloat(c.b), alpha: 1) + } +} + +extension ClaudeCodeStatusPopoverViewController: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return sessions.count + } +} + +extension ClaudeCodeStatusPopoverViewController: NSTableViewDelegate { + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let status = sessions[row] + let identifier = tableColumn?.identifier ?? NSUserInterfaceItemIdentifier("name") + + let cellID = NSUserInterfaceItemIdentifier("ClaudeCell-\(identifier.rawValue)") + let cell: NSTableCellView + if let reused = tableView.makeView(withIdentifier: cellID, owner: self) as? NSTableCellView { + cell = reused + } else { + cell = NSTableCellView() + cell.identifier = cellID + + let textField = NSTextField(labelWithString: "") + textField.translatesAutoresizingMaskIntoConstraints = false + textField.lineBreakMode = .byTruncatingTail + cell.textField = textField + cell.addSubview(textField) + + NSLayoutConstraint.activate([ + textField.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 4), + textField.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4), + textField.centerYAnchor.constraint(equalTo: cell.centerYAnchor), + ]) + } + + if identifier.rawValue == "name" { + cell.textField?.stringValue = sessionName(for: status.sessionID) + if let color = dotColor(for: status) { + cell.textField?.textColor = color + } else { + cell.textField?.textColor = .labelColor + } + } else { + cell.textField?.stringValue = status.statusText ?? "" + cell.textField?.textColor = .secondaryLabelColor + } + + return cell + } + + func tableViewSelectionDidChange(_ notification: Notification) { + let row = tableView.selectedRow + guard row >= 0 && row < sessions.count else { return } + let sessionID = sessions[row].sessionID + iTermController.sharedInstance()?.revealSession(withGUID: sessionID) + view.window?.performClose(nil) + } +} diff --git a/sources/iTermStatusBarClaudeCodeComponent.swift b/sources/iTermStatusBarClaudeCodeComponent.swift new file mode 100644 index 0000000000..e40adda4a9 --- /dev/null +++ b/sources/iTermStatusBarClaudeCodeComponent.swift @@ -0,0 +1,135 @@ +import AppKit + +/// Pure logic extracted from iTermStatusBarClaudeCodeComponent so it can be unit-tested +/// without instantiating the heavyweight status-bar component hierarchy. +enum ClaudeCodeSummaryBuilder { + static let waitingText = "Waiting" + static let workingText = "Working\u{2026}" + static let idleText = "Idle" + + static func isClaudeCodeStatus(_ text: String?) -> Bool { + guard let text else { return false } + return text == waitingText || text == workingText || text == idleText + } + + static func buildSummary(from sessions: [iTermSessionTabStatus]) -> String { + if sessions.isEmpty { + return "No sessions" + } + let waiting = sessions.filter { $0.statusText == waitingText }.count + let working = sessions.filter { $0.statusText == workingText }.count + let idle = sessions.filter { $0.statusText == idleText }.count + + var parts = [String]() + if waiting > 0 { parts.append(waiting == 1 ? "1 waiting" : "\(waiting) waiting") } + if working > 0 { parts.append(working == 1 ? "1 working" : "\(working) working") } + if idle > 0 { parts.append(idle == 1 ? "1 idle" : "\(idle) idle") } + return parts.joined(separator: ", ") + } +} + +@objc(iTermStatusBarClaudeCodeComponent) +class iTermStatusBarClaudeCodeComponent: iTermStatusBarTextComponent { + private var observerToken: NotifyingDictionaryObserverToken? + private var cachedSummary: String = "" + private var popover: NSPopover? + + override static var compatibleProfileTypes: ProfileType { + [.terminal] + } + + required init(configuration: [iTermStatusBarComponentConfigurationKey: Any], scope: iTermVariableScope?) { + super.init(configuration: configuration, scope: scope) + observerToken = SessionStatusController.instance.addObserver { [weak self] _, _, _ in + DispatchQueue.main.async { + self?.rebuildSummary() + } + } + rebuildSummary() + } + + required init?(coder: NSCoder) { + it_fatalError("init(coder:) has not been implemented") + } + + deinit { + observerToken = nil + } + + override func statusBarComponentIcon() -> NSImage { + return NSImage(systemSymbolName: "brain", accessibilityDescription: "Claude Code") ?? NSImage() + } + + override func statusBarComponentShortDescription() -> String { + return "Claude Code" + } + + override func statusBarComponentDetailedDescription() -> String { + return "Shows status of Claude Code sessions across all windows" + } + + override func statusBarComponentExemplar(withBackgroundColor backgroundColor: NSColor, textColor: NSColor) -> Any { + return "2 waiting, 1 working" + } + + override func statusBarComponentCanStretch() -> Bool { + return false + } + + override func statusBarComponentHandlesClicks() -> Bool { + return true + } + + override var stringVariants: [String]? { + return [cachedSummary] + } + + override func stringValueForCurrentWidth() -> String? { + return cachedSummary + } + + override func statusBarComponentUpdate() { + rebuildSummary() + super.statusBarComponentUpdate() + } + + override func statusBarComponentDidClick(with view: NSView) { + let sessions = claudeCodeSessions() + if sessions.count == 1, let only = sessions.first { + iTermController.sharedInstance()?.revealSession(withGUID: only.sessionID) + } else { + showPopover(relativeTo: view) + } + } + + private func rebuildSummary() { + cachedSummary = ClaudeCodeSummaryBuilder.buildSummary(from: claudeCodeSessions()) + updateTextFieldIfNeeded() + } + + private func claudeCodeSessions() -> [iTermSessionTabStatus] { + return SessionStatusController.instance.statuses.values.filter { + ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) + } + } + + private func showPopover(relativeTo view: NSView) { + popover?.close() + + let newPopover = NSPopover() + newPopover.appearance = view.effectiveAppearance + newPopover.behavior = .semitransient + let viewController = ClaudeCodeStatusPopoverViewController() + newPopover.contentViewController = viewController + newPopover.contentSize = viewController.preferredContentSize + + let positionRawValue = iTermPreferences.unsignedInteger(forKey: kPreferenceKeyStatusBarPosition) + let preferredEdge: NSRectEdge = positionRawValue == iTermStatusBarPosition.top.rawValue ? .maxY : .minY + + let relativeView = view.subviews.first ?? view + var rect = relativeView.bounds + rect.size.width = statusBarComponentMinimumWidth() + newPopover.show(relativeTo: rect, of: relativeView, preferredEdge: preferredEdge) + popover = newPopover + } +} From a37cf5f6d44026f02dc0ecd2c3a83138b5a1c602 Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 22:49:58 +0100 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=8E=A8=20style(status-bar):=20constra?= =?UTF-8?q?in=20brain=20icon=20to=2011pt=20for=20proper=20spacing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sources/iTermStatusBarClaudeCodeComponent.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sources/iTermStatusBarClaudeCodeComponent.swift b/sources/iTermStatusBarClaudeCodeComponent.swift index e40adda4a9..4cae744edf 100644 --- a/sources/iTermStatusBarClaudeCodeComponent.swift +++ b/sources/iTermStatusBarClaudeCodeComponent.swift @@ -57,7 +57,9 @@ class iTermStatusBarClaudeCodeComponent: iTermStatusBarTextComponent { } override func statusBarComponentIcon() -> NSImage { - return NSImage(systemSymbolName: "brain", accessibilityDescription: "Claude Code") ?? NSImage() + let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .regular) + return NSImage(systemSymbolName: "brain", accessibilityDescription: "Claude Code")? + .withSymbolConfiguration(config) ?? NSImage() } override func statusBarComponentShortDescription() -> String { From 6625007c216e4ec19324f59c3267c9710674ab96 Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 23:26:59 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=90=9B=20fix(status-bar):=20always=20?= =?UTF-8?q?show=20popover=20on=20single=20click=20instead=20of=20navigatin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sources/iTermStatusBarClaudeCodeComponent.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sources/iTermStatusBarClaudeCodeComponent.swift b/sources/iTermStatusBarClaudeCodeComponent.swift index 4cae744edf..369a2a320e 100644 --- a/sources/iTermStatusBarClaudeCodeComponent.swift +++ b/sources/iTermStatusBarClaudeCodeComponent.swift @@ -97,11 +97,8 @@ class iTermStatusBarClaudeCodeComponent: iTermStatusBarTextComponent { override func statusBarComponentDidClick(with view: NSView) { let sessions = claudeCodeSessions() - if sessions.count == 1, let only = sessions.first { - iTermController.sharedInstance()?.revealSession(withGUID: only.sessionID) - } else { - showPopover(relativeTo: view) - } + guard !sessions.isEmpty else { return } + showPopover(relativeTo: view) } private func rebuildSummary() { From 491634900642be539a36f6dcf69c051276c31265 Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 23:33:38 +0100 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=8E=A8=20style(claude-code-popover):?= =?UTF-8?q?=20replace=20status=20text=20with=20pill=20badge=20and=20hand?= =?UTF-8?q?=20cursor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...laudeCodeStatusPopoverViewController.swift | 115 +++++++++++++----- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/sources/ClaudeCodeStatusPopoverViewController.swift b/sources/ClaudeCodeStatusPopoverViewController.swift index c292ef004b..fdbb4dd8b7 100644 --- a/sources/ClaudeCodeStatusPopoverViewController.swift +++ b/sources/ClaudeCodeStatusPopoverViewController.swift @@ -18,10 +18,11 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { scrollView.hasVerticalScroller = true scrollView.autohidesScrollers = true - tableView = NSTableView() + tableView = ClickableTableView() tableView.allowsMultipleSelection = false tableView.headerView = nil tableView.backgroundColor = .clear + tableView.rowHeight = 32 let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) nameColumn.width = 220 @@ -84,58 +85,108 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { return session.name ?? sessionID } - private func dotColor(for status: iTermSessionTabStatus) -> NSColor? { - guard status.hasStatusTextColor else { return nil } + private func pillColor(for status: iTermSessionTabStatus) -> NSColor { + guard status.hasStatusTextColor else { return .secondaryLabelColor } let c = status.statusTextColor return NSColor(srgbRed: CGFloat(c.r), green: CGFloat(c.g), blue: CGFloat(c.b), alpha: 1) } } +// MARK: - Table view with pointing hand cursor + +private class ClickableTableView: NSTableView { + override func resetCursorRects() { + addCursorRect(bounds, cursor: .pointingHand) + } +} + +// MARK: - Pill view + +private class StatusPillView: NSView { + private let label = NSTextField(labelWithString: "") + + init() { + super.init(frame: .zero) + wantsLayer = true + layer?.cornerRadius = 9 + label.translatesAutoresizingMaskIntoConstraints = false + label.font = NSFont.systemFont(ofSize: 11, weight: .medium) + label.textColor = .white + label.alignment = .center + addSubview(label) + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: centerXAnchor), + label.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + } + + required init?(coder: NSCoder) { it_fatalError() } + + func configure(text: String, color: NSColor) { + label.stringValue = text + layer?.backgroundColor = color.withAlphaComponent(0.85).cgColor + } +} + +// MARK: - NSTableViewDataSource + extension ClaudeCodeStatusPopoverViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return sessions.count } } +// MARK: - NSTableViewDelegate + extension ClaudeCodeStatusPopoverViewController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let status = sessions[row] let identifier = tableColumn?.identifier ?? NSUserInterfaceItemIdentifier("name") - let cellID = NSUserInterfaceItemIdentifier("ClaudeCell-\(identifier.rawValue)") - let cell: NSTableCellView - if let reused = tableView.makeView(withIdentifier: cellID, owner: self) as? NSTableCellView { - cell = reused - } else { - cell = NSTableCellView() - cell.identifier = cellID - - let textField = NSTextField(labelWithString: "") - textField.translatesAutoresizingMaskIntoConstraints = false - textField.lineBreakMode = .byTruncatingTail - cell.textField = textField - cell.addSubview(textField) - - NSLayoutConstraint.activate([ - textField.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 4), - textField.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4), - textField.centerYAnchor.constraint(equalTo: cell.centerYAnchor), - ]) - } - if identifier.rawValue == "name" { - cell.textField?.stringValue = sessionName(for: status.sessionID) - if let color = dotColor(for: status) { - cell.textField?.textColor = color + let cellID = NSUserInterfaceItemIdentifier("ClaudeCell-name") + let cell: NSTableCellView + if let reused = tableView.makeView(withIdentifier: cellID, owner: self) as? NSTableCellView { + cell = reused } else { - cell.textField?.textColor = .labelColor + cell = NSTableCellView() + cell.identifier = cellID + let textField = NSTextField(labelWithString: "") + textField.translatesAutoresizingMaskIntoConstraints = false + textField.lineBreakMode = .byTruncatingTail + cell.textField = textField + cell.addSubview(textField) + NSLayoutConstraint.activate([ + textField.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 8), + textField.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4), + textField.centerYAnchor.constraint(equalTo: cell.centerYAnchor), + ]) } + cell.textField?.stringValue = sessionName(for: status.sessionID) + cell.textField?.textColor = .labelColor + return cell + } else { - cell.textField?.stringValue = status.statusText ?? "" - cell.textField?.textColor = .secondaryLabelColor + let cellID = NSUserInterfaceItemIdentifier("ClaudeCell-status") + if let reused = tableView.makeView(withIdentifier: cellID, owner: self) as? NSTableCellView, + let pill = reused.subviews.first as? StatusPillView { + pill.configure(text: status.statusText ?? "", color: pillColor(for: status)) + return reused + } + let cell = NSTableCellView() + cell.identifier = cellID + let pill = StatusPillView() + pill.translatesAutoresizingMaskIntoConstraints = false + cell.addSubview(pill) + NSLayoutConstraint.activate([ + pill.centerYAnchor.constraint(equalTo: cell.centerYAnchor), + pill.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8), + pill.widthAnchor.constraint(equalToConstant: 64), + pill.heightAnchor.constraint(equalToConstant: 18), + ]) + pill.configure(text: status.statusText ?? "", color: pillColor(for: status)) + return cell } - - return cell } func tableViewSelectionDidChange(_ notification: Notification) { From 05010c78165eace7fb8c25ab8d06a3b8b211c0c0 Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 23:40:21 +0100 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=8E=A8=20style(claude-code-status):?= =?UTF-8?q?=20improve=20pill=20visibility=20and=20cursor=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Widen status column to 96pt and increase pill right margin to 16pt to prevent clipping - Disable selection highlight (selectionHighlightStyle = .none) to avoid blue row flash - Replace resetCursorRects with NSTrackingArea + cursorUpdate for consistent pointing hand cursor across entire table view --- ...laudeCodeStatusPopoverViewController.swift | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/sources/ClaudeCodeStatusPopoverViewController.swift b/sources/ClaudeCodeStatusPopoverViewController.swift index fdbb4dd8b7..e6433952a2 100644 --- a/sources/ClaudeCodeStatusPopoverViewController.swift +++ b/sources/ClaudeCodeStatusPopoverViewController.swift @@ -23,15 +23,16 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { tableView.headerView = nil tableView.backgroundColor = .clear tableView.rowHeight = 32 + tableView.selectionHighlightStyle = .none let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) - nameColumn.width = 220 + nameColumn.width = 210 nameColumn.minWidth = 100 tableView.addTableColumn(nameColumn) let statusColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("status")) - statusColumn.width = 84 - statusColumn.minWidth = 60 + statusColumn.width = 96 + statusColumn.minWidth = 70 tableView.addTableColumn(statusColumn) tableView.dataSource = self @@ -92,11 +93,20 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { } } -// MARK: - Table view with pointing hand cursor +// MARK: - Table view with pointing hand cursor and no selection highlight private class ClickableTableView: NSTableView { - override func resetCursorRects() { - addCursorRect(bounds, cursor: .pointingHand) + override func updateTrackingAreas() { + super.updateTrackingAreas() + for area in trackingAreas { removeTrackingArea(area) } + addTrackingArea(NSTrackingArea(rect: bounds, + options: [.cursorUpdate, .activeInActiveApp, .inVisibleRect], + owner: self, + userInfo: nil)) + } + + override func cursorUpdate(with event: NSEvent) { + NSCursor.pointingHand.set() } } @@ -180,7 +190,7 @@ extension ClaudeCodeStatusPopoverViewController: NSTableViewDelegate { cell.addSubview(pill) NSLayoutConstraint.activate([ pill.centerYAnchor.constraint(equalTo: cell.centerYAnchor), - pill.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8), + pill.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -16), pill.widthAnchor.constraint(equalToConstant: 64), pill.heightAnchor.constraint(equalToConstant: 18), ]) From 4aa09d1ce224b5de59ced95cff22a2d509fb5781 Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 23:53:12 +0100 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=8E=A8=20refactor(status-popover):=20?= =?UTF-8?q?rewrite=20table=20view=20with=20custom=20row=20and=20cell=20vie?= =?UTF-8?q?ws=20for=20improved=20layout=20and=20hover=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...laudeCodeStatusPopoverViewController.swift | 188 +++++++++++------- 1 file changed, 121 insertions(+), 67 deletions(-) diff --git a/sources/ClaudeCodeStatusPopoverViewController.swift b/sources/ClaudeCodeStatusPopoverViewController.swift index e6433952a2..b1728daeeb 100644 --- a/sources/ClaudeCodeStatusPopoverViewController.swift +++ b/sources/ClaudeCodeStatusPopoverViewController.swift @@ -17,23 +17,19 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.hasVerticalScroller = true scrollView.autohidesScrollers = true + scrollView.drawsBackground = false - tableView = ClickableTableView() + tableView = SessionTableView() tableView.allowsMultipleSelection = false tableView.headerView = nil tableView.backgroundColor = .clear - tableView.rowHeight = 32 + tableView.rowHeight = 36 tableView.selectionHighlightStyle = .none + tableView.intercellSpacing = NSSize(width: 0, height: 2) - let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) - nameColumn.width = 210 - nameColumn.minWidth = 100 - tableView.addTableColumn(nameColumn) - - let statusColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("status")) - statusColumn.width = 96 - statusColumn.minWidth = 70 - tableView.addTableColumn(statusColumn) + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("session")) + column.minWidth = 200 + tableView.addTableColumn(column) tableView.dataSource = self tableView.delegate = self @@ -42,8 +38,8 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { view.addSubview(scrollView) NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 4), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -4), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), view.widthAnchor.constraint(equalToConstant: preferredContentSize.width), @@ -93,32 +89,116 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { } } -// MARK: - Table view with pointing hand cursor and no selection highlight +// MARK: - Table view + +private class SessionTableView: NSTableView { + private var hoveredRow: Int = -1 -private class ClickableTableView: NSTableView { override func updateTrackingAreas() { super.updateTrackingAreas() for area in trackingAreas { removeTrackingArea(area) } addTrackingArea(NSTrackingArea(rect: bounds, - options: [.cursorUpdate, .activeInActiveApp, .inVisibleRect], + options: [.mouseMoved, .mouseEnteredAndExited, + .cursorUpdate, .activeInActiveApp, .inVisibleRect], owner: self, userInfo: nil)) } override func cursorUpdate(with event: NSEvent) { - NSCursor.pointingHand.set() + let point = convert(event.locationInWindow, from: nil) + if row(at: point) >= 0 { + NSCursor.pointingHand.set() + } + } + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + updateHover(for: event) + } + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + updateHover(for: event) + } + + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + setHoveredRow(-1) + } + + private func updateHover(for event: NSEvent) { + let point = convert(event.locationInWindow, from: nil) + setHoveredRow(row(at: point)) + } + + private func setHoveredRow(_ row: Int) { + guard row != hoveredRow else { return } + let previous = hoveredRow + hoveredRow = row + if previous >= 0 { + (rowView(atRow: previous, makeIfNecessary: false) as? SessionRowView)?.isHovered = false + } + if row >= 0 { + (rowView(atRow: row, makeIfNecessary: false) as? SessionRowView)?.isHovered = true + } } } +// MARK: - Row view with hover + +private class SessionRowView: NSTableRowView { + var isHovered = false { + didSet { needsDisplay = true } + } + + override var isOpaque: Bool { false } + + override func drawBackground(in dirtyRect: NSRect) { + guard isHovered else { return } + NSColor.white.withAlphaComponent(0.07).setFill() + NSBezierPath(roundedRect: bounds.insetBy(dx: 6, dy: 1), xRadius: 6, yRadius: 6).fill() + } +} + +// MARK: - Session cell + +private class SessionCellView: NSTableCellView { + let nameLabel = NSTextField(labelWithString: "") + let pill = StatusPillView() + + override init(frame: NSRect) { + super.init(frame: frame) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.lineBreakMode = .byTruncatingTail + nameLabel.textColor = .labelColor + nameLabel.font = NSFont.systemFont(ofSize: 13) + textField = nameLabel + addSubview(nameLabel) + addSubview(pill) + pill.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + pill.centerYAnchor.constraint(equalTo: centerYAnchor), + pill.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), + pill.widthAnchor.constraint(equalToConstant: 68), + pill.heightAnchor.constraint(equalToConstant: 20), + nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), + nameLabel.trailingAnchor.constraint(equalTo: pill.leadingAnchor, constant: -8), + nameLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + } + + required init?(coder: NSCoder) { it_fatalError() } +} + // MARK: - Pill view private class StatusPillView: NSView { private let label = NSTextField(labelWithString: "") - init() { - super.init(frame: .zero) + override init(frame: NSRect) { + super.init(frame: frame) wantsLayer = true - layer?.cornerRadius = 9 + layer?.cornerRadius = 10 label.translatesAutoresizingMaskIntoConstraints = false label.font = NSFont.systemFont(ofSize: 11, weight: .medium) label.textColor = .white @@ -142,68 +222,42 @@ private class StatusPillView: NSView { extension ClaudeCodeStatusPopoverViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { - return sessions.count + sessions.count } } // MARK: - NSTableViewDelegate extension ClaudeCodeStatusPopoverViewController: NSTableViewDelegate { + func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { + let id = NSUserInterfaceItemIdentifier("SessionRow") + if let reused = tableView.makeView(withIdentifier: id, owner: self) as? SessionRowView { + return reused + } + let rowView = SessionRowView() + rowView.identifier = id + return rowView + } + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let status = sessions[row] - let identifier = tableColumn?.identifier ?? NSUserInterfaceItemIdentifier("name") - - if identifier.rawValue == "name" { - let cellID = NSUserInterfaceItemIdentifier("ClaudeCell-name") - let cell: NSTableCellView - if let reused = tableView.makeView(withIdentifier: cellID, owner: self) as? NSTableCellView { - cell = reused - } else { - cell = NSTableCellView() - cell.identifier = cellID - let textField = NSTextField(labelWithString: "") - textField.translatesAutoresizingMaskIntoConstraints = false - textField.lineBreakMode = .byTruncatingTail - cell.textField = textField - cell.addSubview(textField) - NSLayoutConstraint.activate([ - textField.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 8), - textField.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4), - textField.centerYAnchor.constraint(equalTo: cell.centerYAnchor), - ]) - } - cell.textField?.stringValue = sessionName(for: status.sessionID) - cell.textField?.textColor = .labelColor - return cell - + let id = NSUserInterfaceItemIdentifier("SessionCell") + let cell: SessionCellView + if let reused = tableView.makeView(withIdentifier: id, owner: self) as? SessionCellView { + cell = reused } else { - let cellID = NSUserInterfaceItemIdentifier("ClaudeCell-status") - if let reused = tableView.makeView(withIdentifier: cellID, owner: self) as? NSTableCellView, - let pill = reused.subviews.first as? StatusPillView { - pill.configure(text: status.statusText ?? "", color: pillColor(for: status)) - return reused - } - let cell = NSTableCellView() - cell.identifier = cellID - let pill = StatusPillView() - pill.translatesAutoresizingMaskIntoConstraints = false - cell.addSubview(pill) - NSLayoutConstraint.activate([ - pill.centerYAnchor.constraint(equalTo: cell.centerYAnchor), - pill.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -16), - pill.widthAnchor.constraint(equalToConstant: 64), - pill.heightAnchor.constraint(equalToConstant: 18), - ]) - pill.configure(text: status.statusText ?? "", color: pillColor(for: status)) - return cell + cell = SessionCellView() + cell.identifier = id } + cell.nameLabel.stringValue = sessionName(for: status.sessionID) + cell.pill.configure(text: status.statusText ?? "", color: pillColor(for: status)) + return cell } func tableViewSelectionDidChange(_ notification: Notification) { let row = tableView.selectedRow guard row >= 0 && row < sessions.count else { return } - let sessionID = sessions[row].sessionID - iTermController.sharedInstance()?.revealSession(withGUID: sessionID) + iTermController.sharedInstance()?.revealSession(withGUID: sessions[row].sessionID) view.window?.performClose(nil) } } From 540921d8928a303ccc72b2c89edfccaa31211bc3 Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Thu, 9 Apr 2026 23:58:01 +0100 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=90=9B=20fix(claude-code-status):=20f?= =?UTF-8?q?ilter=20sessions=20to=20only=20show=20active=20Claude=20jobs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sources/ClaudeCodeStatusPopoverViewController.swift | 3 ++- sources/iTermStatusBarClaudeCodeComponent.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sources/ClaudeCodeStatusPopoverViewController.swift b/sources/ClaudeCodeStatusPopoverViewController.swift index b1728daeeb..60b9c3e485 100644 --- a/sources/ClaudeCodeStatusPopoverViewController.swift +++ b/sources/ClaudeCodeStatusPopoverViewController.swift @@ -58,8 +58,9 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { } private func claudeCodeSessions() -> [iTermSessionTabStatus] { + let activeGUIDs = GlobalJobMonitor.instance.sessionGUIDs(runningJob: "claude") let filtered = SessionStatusController.instance.statuses.values.filter { - ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) + activeGUIDs.contains($0.sessionID) && ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) } return filtered.sorted { lhs, rhs in sortOrder(lhs.statusText) < sortOrder(rhs.statusText) diff --git a/sources/iTermStatusBarClaudeCodeComponent.swift b/sources/iTermStatusBarClaudeCodeComponent.swift index 369a2a320e..22ea5de0e7 100644 --- a/sources/iTermStatusBarClaudeCodeComponent.swift +++ b/sources/iTermStatusBarClaudeCodeComponent.swift @@ -107,8 +107,9 @@ class iTermStatusBarClaudeCodeComponent: iTermStatusBarTextComponent { } private func claudeCodeSessions() -> [iTermSessionTabStatus] { + let activeGUIDs = GlobalJobMonitor.instance.sessionGUIDs(runningJob: "claude") return SessionStatusController.instance.statuses.values.filter { - ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) + activeGUIDs.contains($0.sessionID) && ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) } } From ba1c871fcd4c996bfeb9883321f6b919b994c40c Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Fri, 10 Apr 2026 00:03:06 +0100 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=94=A7=20fix(claude-code-status):=20r?= =?UTF-8?q?evert=20GlobalJobMonitor=20filter=20to=20fix=20popup=20timing?= =?UTF-8?q?=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sources/ClaudeCodeStatusPopoverViewController.swift | 3 +-- sources/iTermStatusBarClaudeCodeComponent.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sources/ClaudeCodeStatusPopoverViewController.swift b/sources/ClaudeCodeStatusPopoverViewController.swift index 60b9c3e485..b1728daeeb 100644 --- a/sources/ClaudeCodeStatusPopoverViewController.swift +++ b/sources/ClaudeCodeStatusPopoverViewController.swift @@ -58,9 +58,8 @@ class ClaudeCodeStatusPopoverViewController: NSViewController { } private func claudeCodeSessions() -> [iTermSessionTabStatus] { - let activeGUIDs = GlobalJobMonitor.instance.sessionGUIDs(runningJob: "claude") let filtered = SessionStatusController.instance.statuses.values.filter { - activeGUIDs.contains($0.sessionID) && ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) + ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) } return filtered.sorted { lhs, rhs in sortOrder(lhs.statusText) < sortOrder(rhs.statusText) diff --git a/sources/iTermStatusBarClaudeCodeComponent.swift b/sources/iTermStatusBarClaudeCodeComponent.swift index 22ea5de0e7..369a2a320e 100644 --- a/sources/iTermStatusBarClaudeCodeComponent.swift +++ b/sources/iTermStatusBarClaudeCodeComponent.swift @@ -107,9 +107,8 @@ class iTermStatusBarClaudeCodeComponent: iTermStatusBarTextComponent { } private func claudeCodeSessions() -> [iTermSessionTabStatus] { - let activeGUIDs = GlobalJobMonitor.instance.sessionGUIDs(runningJob: "claude") return SessionStatusController.instance.statuses.values.filter { - activeGUIDs.contains($0.sessionID) && ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) + ClaudeCodeSummaryBuilder.isClaudeCodeStatus($0.statusText) } } From dd17335aa94768dc896e68e0e3b711e08300022c Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Fri, 10 Apr 2026 00:08:09 +0100 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20chore/project:=20ad?= =?UTF-8?q?d=20ClaudeCode=20status=20bar=20Swift=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register new Swift files iTermStatusBarClaudeCodeComponent.swift and ClaudeCodeStatusPopoverViewController.swift in Xcode project and status bar setup, integrating them into build phases and UI registration. This change updates iTerm2.xcodeproj/project.pbxproj and sources/iTermStatusBarSetupViewController.m to support new components. --- iTerm2.xcodeproj/project.pbxproj | 8 ++++++++ sources/iTermStatusBarSetupViewController.m | 1 + 2 files changed, 9 insertions(+) diff --git a/iTerm2.xcodeproj/project.pbxproj b/iTerm2.xcodeproj/project.pbxproj index 6216651626..1a07d12b97 100644 --- a/iTerm2.xcodeproj/project.pbxproj +++ b/iTerm2.xcodeproj/project.pbxproj @@ -674,6 +674,7 @@ 4705C05130977E2C81FD75E8 /* iTermUnicodeNormalization.h in Sources */ = {isa = PBXBuildFile; fileRef = 3335AD96138BA74CC79D25A8 /* iTermUnicodeNormalization.h */; }; 4887C7B3CAB5465296225860 /* MenuTips.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 904AE312A0E0489899D9ECB8 /* MenuTips.xcassets */; }; 49A6E4091211CC6000D9AD6F /* Compatability.h in Headers */ = {isa = PBXBuildFile; fileRef = 49A6E4081211CC6000D9AD6F /* Compatability.h */; }; + 4B395950B0EF00428363FA75 /* iTermStatusBarClaudeCodeComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854E1EAA1B9E2B6D8BA548D0 /* iTermStatusBarClaudeCodeComponent.swift */; }; 4BC1E97CEA4441CF9390DD52 /* MenuTips.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 904AE312A0E0489899D9ECB8 /* MenuTips.xcassets */; }; 4C4BE71D1C8948BD8CD672BC /* iTermLayoutCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = AF19E441FF824539A275D8DA /* iTermLayoutCalculator.m */; }; 4C9D9EF0E49226A8A974CE88 /* SearchResultSoftBoundaryExtender.swift in Sources */ = {isa = PBXBuildFile; fileRef = A004AF3FD82E9DE92D43DF5E /* SearchResultSoftBoundaryExtender.swift */; }; @@ -5183,6 +5184,7 @@ A7E25DCB1956D4A7FA738CB9 /* iTermStreamingPNGWriter.h in Sources */ = {isa = PBXBuildFile; fileRef = CB2F48F836C5732146BCEDD9 /* iTermStreamingPNGWriter.h */; }; AA190CE70997D5318EF50177 /* ClaudeCodeOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E961A126DE6D9EFA0C6CC9AB /* ClaudeCodeOnboarding.swift */; }; B2CA85E23B4CCCC3391BA025 /* NotifyingDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877B9444B69DE32D9CF87354 /* NotifyingDictionary.swift */; }; + B9E5F0B3A747133496AC5145 /* ClaudeCodeStatusPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E743E798CA3A96AB8F6168 /* ClaudeCodeStatusPopoverViewController.swift */; }; C48D7AA2E70B019313C401F3 /* WordSelectionAtom.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08AA4008946722C3F4C67C1 /* WordSelectionAtom.swift */; }; C5F394249CF581965B22B714 /* GlobalJobMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA34A6F29B10471D8FD572F6 /* GlobalJobMonitor.swift */; }; C6675EBC1C4FE96B0041173B /* iTermSelectorSwizzler.m in Sources */ = {isa = PBXBuildFile; fileRef = C6675EBB1C4FE96B0041173B /* iTermSelectorSwizzler.m */; }; @@ -6311,6 +6313,7 @@ 7F3AA921DF1744D7BA890104 /* PillBackgroundRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillBackgroundRenderer.swift; sourceTree = ""; }; 80A64098B399CE71B981E2C7 /* iTermRenderingComparer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = iTermRenderingComparer.swift; sourceTree = ""; }; 81E145AE29B68F8F2A69891A /* iTermLocatedString+ScreenCharArray.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "iTermLocatedString+ScreenCharArray.swift"; sourceTree = ""; }; + 854E1EAA1B9E2B6D8BA548D0 /* iTermStatusBarClaudeCodeComponent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = iTermStatusBarClaudeCodeComponent.swift; sourceTree = ""; }; 86FC661122588ABDA0EE3245 /* PTYTextViewAccessibilityTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PTYTextViewAccessibilityTest.m; sourceTree = ""; }; 8742065A0564169600CFC3F1 /* iTerm2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iTerm2.app; sourceTree = BUILT_PRODUCTS_DIR; }; 877B9444B69DE32D9CF87354 /* NotifyingDictionary.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotifyingDictionary.swift; sourceTree = ""; }; @@ -9235,6 +9238,7 @@ C6675EBA1C4FE96B0041173B /* iTermSelectorSwizzler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iTermSelectorSwizzler.h; sourceTree = ""; }; C6675EBB1C4FE96B0041173B /* iTermSelectorSwizzler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iTermSelectorSwizzler.m; sourceTree = ""; }; C80C997508DC4BF559F599EB /* UnderlineCompositeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UnderlineCompositeRenderer.swift; sourceTree = ""; }; + C9E743E798CA3A96AB8F6168 /* ClaudeCodeStatusPopoverViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ClaudeCodeStatusPopoverViewController.swift; sourceTree = ""; }; CA1C7B91071000DE37B9DA33 /* iTermSessionTabStatus.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = iTermSessionTabStatus.swift; sourceTree = ""; }; CA34A6F29B10471D8FD572F6 /* GlobalJobMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlobalJobMonitor.swift; sourceTree = ""; }; CB2F48F836C5732146BCEDD9 /* iTermStreamingPNGWriter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = iTermStreamingPNGWriter.h; sourceTree = ""; }; @@ -9623,6 +9627,8 @@ E961A126DE6D9EFA0C6CC9AB /* ClaudeCodeOnboarding.swift */, 81E145AE29B68F8F2A69891A /* iTermLocatedString+ScreenCharArray.swift */, AF73E7832C84F926A6CE54CA /* iTermOptionalPathRecognizer.swift */, + 854E1EAA1B9E2B6D8BA548D0 /* iTermStatusBarClaudeCodeComponent.swift */, + C9E743E798CA3A96AB8F6168 /* ClaudeCodeStatusPopoverViewController.swift */, ); name = Classes; path = sources/; @@ -20262,6 +20268,8 @@ AA190CE70997D5318EF50177 /* ClaudeCodeOnboarding.swift in Sources */, 8E9796208C12DD69744D3606 /* iTermLocatedString+ScreenCharArray.swift in Sources */, D7391EB323E94EAC34943C01 /* iTermOptionalPathRecognizer.swift in Sources */, + 4B395950B0EF00428363FA75 /* iTermStatusBarClaudeCodeComponent.swift in Sources */, + B9E5F0B3A747133496AC5145 /* ClaudeCodeStatusPopoverViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/sources/iTermStatusBarSetupViewController.m b/sources/iTermStatusBarSetupViewController.m index d7e8fcbe79..b005165e62 100644 --- a/sources/iTermStatusBarSetupViewController.m +++ b/sources/iTermStatusBarSetupViewController.m @@ -115,6 +115,7 @@ - (void)loadElements { [iTermStatusBarActionMenuComponent class], [iTermStatusBarSnippetMenuComponent class], [iTermStatusBarTriggersComponent class], + [iTermStatusBarClaudeCodeComponent class], [iTermStatusBarGitComponent class], [iTermStatusBarHostnameComponent class],