Skip to content

Commit 75def1b

Browse files
feat(session-tab): list every PR opened in a session, skip green tint on active tab
The session detail row only displayed one PR — the resolver's best match for the current branch — so PRs opened earlier in the session disappeared after a branch switch. Each Session now accumulates every distinct PR it observes (by number), upserting state on refresh, and the detail row renders one clickable badge per PR in detection order. Also suppress the "work done" green tint on the currently active session: the user is already looking at it, so changing its color on completion is just noise. Other colors still apply on the active tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b14497 commit 75def1b

1 file changed

Lines changed: 71 additions & 37 deletions

File tree

Sources/NeetlyApp/SessionWindowController.swift

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ class Session {
88
var fileWatcher: FileWatcher?
99
/// Status color for the session tab. nil = default, green = done, etc.
1010
var statusColor: NSColor?
11-
/// Resolved GitHub PR info. nil = no PR found or not yet fetched.
12-
var prInfo: GitHubPRInfo?
11+
/// All GitHub PRs observed during this session's lifetime. The resolver only
12+
/// surfaces the PR for the currently-checked-out branch; this list accumulates
13+
/// every distinct PR (by number) seen across branch switches so users can
14+
/// still navigate to PRs opened earlier in the session.
15+
var prInfos: [GitHubPRInfo] = []
1316
/// Short commit SHA of the worktree's current HEAD.
1417
var commitSha: String?
1518
/// GitHub commit URL for the worktree's current HEAD, if the remote is GitHub.
@@ -64,44 +67,59 @@ class Session {
6467
}
6568

6669
func refreshPRStatus() {
67-
let previousPR = prInfo
6870
GitHubPRResolver.resolve(worktreePath: config.repoPath) { [weak self] info in
6971
guard let self = self else { return }
70-
self.prInfo = info
72+
guard let info = info else {
73+
// Resolver found nothing for the current branch — don't clear the
74+
// session's PR history; previously-opened PRs stay listed.
75+
self.onStatusChanged?()
76+
return
77+
}
78+
79+
let existingIdx = self.prInfos.firstIndex { $0.number == info.number }
80+
let previousState = existingIdx.map { self.prInfos[$0].state }
81+
82+
if let idx = existingIdx {
83+
self.prInfos[idx] = info
84+
} else {
85+
self.prInfos.append(info)
86+
}
87+
88+
// Setup screen still shows a single per-session PR — keep writing the
89+
// latest detected one.
7190
SessionStore.shared.updatePRInfo(
7291
repoPath: self.config.repoPath,
7392
worktreeName: self.config.worktreeName,
7493
prInfo: info
7594
)
76-
if let info = info {
77-
let stateLabel: String
78-
switch info.state {
79-
case .open: stateLabel = "Open"
80-
case .draft: stateLabel = "Draft"
81-
case .merged: stateLabel = "Merged"
82-
case .closed: stateLabel = "Closed"
83-
}
8495

85-
if previousPR == nil {
86-
// Log activity when a PR is first detected
87-
ActivityStore.shared.log(
88-
.prOpened,
89-
repoName: self.config.repoName,
90-
detail: "\(info.number)",
91-
prURL: info.url
92-
)
93-
}
96+
let stateLabel: String
97+
switch info.state {
98+
case .open: stateLabel = "Open"
99+
case .draft: stateLabel = "Draft"
100+
case .merged: stateLabel = "Merged"
101+
case .closed: stateLabel = "Closed"
102+
}
94103

95-
// Update state if it changed (e.g., Open → Merged)
96-
if previousPR?.state != info.state || previousPR == nil {
97-
ActivityStore.shared.updatePRState(
98-
repoName: self.config.repoName,
99-
prNumber: "\(info.number)",
100-
state: stateLabel,
101-
url: info.url
102-
)
103-
}
104+
let isNew = existingIdx == nil
105+
if isNew {
106+
ActivityStore.shared.log(
107+
.prOpened,
108+
repoName: self.config.repoName,
109+
detail: "\(info.number)",
110+
prURL: info.url
111+
)
112+
}
113+
114+
if isNew || previousState != info.state {
115+
ActivityStore.shared.updatePRState(
116+
repoName: self.config.repoName,
117+
prNumber: "\(info.number)",
118+
state: stateLabel,
119+
url: info.url
120+
)
104121
}
122+
105123
self.onStatusChanged?()
106124
}
107125
}
@@ -181,7 +199,7 @@ class SessionTabBar: NSView {
181199
@available(*, unavailable)
182200
required init?(coder: NSCoder) { fatalError() }
183201

184-
func update(sessions: [(repoName: String, sessionName: String, commitSha: String?, commitURL: String?, isActive: Bool, statusColor: NSColor?, prInfo: GitHubPRInfo?, diffStats: (added: Int, deleted: Int)?)]) {
202+
func update(sessions: [(repoName: String, sessionName: String, commitSha: String?, commitURL: String?, isActive: Bool, statusColor: NSColor?, prInfos: [GitHubPRInfo], diffStats: (added: Int, deleted: Int)?)]) {
185203
tabViews.forEach { $0.removeFromSuperview() }
186204
tabViews.removeAll()
187205
detailViews.forEach { $0.removeFromSuperview() }
@@ -266,7 +284,8 @@ class SessionTabBar: NSView {
266284
}
267285
}
268286

269-
if let pr = active.prInfo {
287+
prURLsByButton.removeAll()
288+
for pr in active.prInfos {
270289
let prColor = SessionTab.color(for: pr.state)
271290
let stateText = SessionTab.stateLabel(for: pr.state)
272291

@@ -289,8 +308,13 @@ class SessionTabBar: NSView {
289308
prBtn.frame = NSRect(x: detailX, y: centerY, width: prBtn.intrinsicContentSize.width, height: itemHeight)
290309
addSubview(prBtn)
291310
detailViews.append(prBtn)
292-
prInfoURL = URL(string: pr.url)
293-
detailX += prBtn.frame.width + 12
311+
if let url = URL(string: pr.url) {
312+
prURLsByButton[prBtn] = url
313+
}
314+
detailX += prBtn.frame.width + 6
315+
}
316+
if !active.prInfos.isEmpty {
317+
detailX += 6
294318
}
295319

296320
// -- Diff stats (+N -M) --
@@ -322,14 +346,15 @@ class SessionTabBar: NSView {
322346
}
323347

324348
private var commitURL: URL?
325-
private var prInfoURL: URL?
349+
private var prURLsByButton: [NSButton: URL] = [:]
326350

327351
@objc private func openCommitURL(_ sender: Any?) {
328352
if let url = commitURL { NSWorkspace.shared.open(url) }
329353
}
330354

331355
@objc private func openPRURL(_ sender: Any?) {
332-
if let url = prInfoURL { NSWorkspace.shared.open(url) }
356+
guard let btn = sender as? NSButton, let url = prURLsByButton[btn] else { return }
357+
NSWorkspace.shared.open(url)
333358
}
334359

335360
@objc private func plusClicked() {
@@ -664,7 +689,7 @@ class SessionWindowController: NSWindowController {
664689

665690
private func refreshTabBar() {
666691
let tabs = sessions.enumerated().map { (i, ws) in
667-
(repoName: ws.config.repoName, sessionName: ws.config.sessionName, commitSha: ws.commitSha, commitURL: ws.commitURL, isActive: i == activeIndex, statusColor: ws.statusColor, prInfo: ws.prInfo, diffStats: ws.diffStats)
692+
(repoName: ws.config.repoName, sessionName: ws.config.sessionName, commitSha: ws.commitSha, commitURL: ws.commitURL, isActive: i == activeIndex, statusColor: ws.statusColor, prInfos: ws.prInfos, diffStats: ws.diffStats)
668693
}
669694
sessionTabBar.update(sessions: tabs)
670695
}
@@ -712,6 +737,15 @@ class SessionWindowController: NSWindowController {
712737

713738
case "session.notify":
714739
let colorName = command.command ?? "green"
740+
741+
// "work done" on the active session is just noise — the user is
742+
// already looking at it. Drop the green tint so the active tab
743+
// doesn't suddenly change color on completion.
744+
let isActive = activeIndex >= 0 && activeIndex < sessions.count && sessions[activeIndex] === ws
745+
if isActive && colorName == "green" {
746+
return nil
747+
}
748+
715749
let color: NSColor
716750
switch colorName {
717751
case "green": color = NSColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0)

0 commit comments

Comments
 (0)