From 34494ee796cb9d5a68624b0da77a1aa62a90297c Mon Sep 17 00:00:00 2001 From: omartelo Date: Sat, 24 Oct 2026 10:56:52 -0300 Subject: [PATCH] fix(native): open a card's pull request where it belongs The badge reads the pull request where the session's shell is, and a click on it, its card menu item or the footer button looked that number up on Pulls, which lists the project's pull requests. After a cd into another repository that opened the wrong pull request, or none. A shell outside the session's checkout now opens the pull request in the browser. --- CHANGELOG.md | 1 + native/cmd/lich-native/cardmenu.go | 5 +++-- native/cmd/lich-native/cardmenu_test.go | 27 +++++++++++++++++++++++++ native/cmd/lich-native/footer.go | 6 +++--- native/cmd/lich-native/main.go | 6 ++---- native/cmd/lich-native/pulls_actions.go | 24 ++++++++++++++++++++++ native/cmd/lich-native/sidebar.go | 5 ++++- native/cmd/lich-native/sidebar_card.go | 3 ++- 8 files changed, 66 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9795761..0e6e2923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A pull request badge on a session card, its Pull request menu item and the footer's pull request button no longer open the wrong pull request after the session's shell moved into another repository. The badge shows that repository's pull request, and the click used to look its number up among the project's own on Pulls; it now opens the pull request on GitHub instead. - The collapsed sidebar shows the profile new sessions are born in, at the bottom of the rail, and opens the same menu the open sidebar's footer does. It used to show no profile at all. - A tab in the top bar can no longer get stuck lifted, drawn in the accent color as if it were being dragged, with no tab moving again until lich restarted. A tab focused by a click and then given Space or Enter was picked up to reorder from the keyboard, and once a click elsewhere took the keyboard it could neither be dropped nor cancelled; it now goes back to its place when it loses focus. A layout tab renamed in the middle of a drag no longer gets stuck the same way. - A session card no longer grows and shrinks as its agent starts and stops a tool, waits on you or compacts, pushing every card below it up and down. That line now takes the place of the card's path while it shows, and the path comes back when it goes. diff --git a/native/cmd/lich-native/cardmenu.go b/native/cmd/lich-native/cardmenu.go index 0210af4c..1073871d 100644 --- a/native/cmd/lich-native/cardmenu.go +++ b/native/cmd/lich-native/cardmenu.go @@ -149,8 +149,9 @@ func forkItem(sv *sessionView, act *sidebarAction) (ui.MenuEntry, bool) { func pullRequestItem(sv *sessionView, act *sidebarAction) ui.MenuEntry { item := ui.MenuEntry{Label: i18n.T("sidebar.sessionCard.pullRequest"), Icon: icMenuPR, Disabled: sv.pr == nil} if sv.pr != nil { - id, number := sv.ID, sv.pr.Number - item.OnClick = func() { act.activate, act.openPR = id, number } + id := sv.ID + number, url := prTarget(sv.pr, sv.shown, sv.path) + item.OnClick = func() { act.activate, act.openPR, act.openPRURL = id, number, url } } return item } diff --git a/native/cmd/lich-native/cardmenu_test.go b/native/cmd/lich-native/cardmenu_test.go index 74c35c1c..18efa039 100644 --- a/native/cmd/lich-native/cardmenu_test.go +++ b/native/cmd/lich-native/cardmenu_test.go @@ -116,3 +116,30 @@ func TestCardMenuForkEntry(t *testing.T) { } } } + +// A shell that cd'd into another repository has that repository's pull request +// on its card; its number means nothing on the Pulls screen, which lists the +// project's, so the click opens it in the browser. +func TestTheCardsPullRequestOpensWhereItBelongs(t *testing.T) { + pr := &backend.PullRequest{Number: 7, URL: "https://github.com/o/r/pull/7"} + cases := []struct { + name, shown string + number int + url string + }{ + {"in its checkout", "/wt/feat", 7, ""}, + {"in a folder of its checkout", "/wt/feat/internal", 7, ""}, + {"in another repository", "/src/other", 0, pr.URL}, + {"in a sibling with the same prefix", "/wt/feat-2", 0, pr.URL}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + sv := &sessionView{Session: backend.Session{ID: "s1"}, path: "/wt/feat", shown: c.shown, pr: pr} + var act sidebarAction + pullRequestItem(sv, &act).OnClick() + if act.activate != "s1" || act.openPR != c.number || act.openPRURL != c.url { + t.Fatalf("click asked activate %q, Pulls #%d, browser %q; want s1, #%d, %q", act.activate, act.openPR, act.openPRURL, c.number, c.url) + } + }) + } +} diff --git a/native/cmd/lich-native/footer.go b/native/cmd/lich-native/footer.go index 1a609390..64568029 100644 --- a/native/cmd/lich-native/footer.go +++ b/native/cmd/lich-native/footer.go @@ -32,8 +32,8 @@ type footer struct { client *backend.Client root *ui.Root invalidate func() - // openPullRequest opens the Pulls screen on a pull request. - openPullRequest func(number int) + // openPullRequest opens a pull request where prTarget says. + openPullRequest func(number int, url string) // showDelegates lays a session's delegates out beside it. showDelegates func(id string) usage *footerUsage @@ -221,7 +221,7 @@ func (f *footer) act(gtx C, in footerInput) { f.w("files").pressed = f.dock.tab == dockCode f.w("changes").pressed = f.dock.tab == dockReview if f.w("pr").click.Clicked(gtx) && in.pr != nil && f.openPullRequest != nil { - f.openPullRequest(in.pr.Number) + f.openPullRequest(prTarget(in.pr, in.path, in.checkout)) } if f.w("delegates").click.Clicked(gtx) && in.delegates > 0 { f.showDelegates(in.sessionID) diff --git a/native/cmd/lich-native/main.go b/native/cmd/lich-native/main.go index b9f1e562..9c2404a5 100644 --- a/native/cmd/lich-native/main.go +++ b/native/cmd/lich-native/main.go @@ -295,7 +295,7 @@ func run(w *app.Window, rt backend.Runtime) error { u.settings.theme = newThemePicker(ctx, client, u.appearance, &u.toaster, &u.root) u.dock.tab = map[string]dockTab{"code": dockCode, "review": dockReview}[*flagTab] u.footer.dock = u.dock - u.footer.openPullRequest = u.showPulls + u.footer.openPullRequest = u.openPR u.footer.writePaste = u.terms.writePaste u.footer.showDelegates = func(id string) { u.layoutStage.delegates = id } u.settings.footer = u.footer @@ -894,9 +894,7 @@ func (u *window) apply(act sidebarAction) { if act.close != "" { u.closeSession(act.close) } - if act.openPR != 0 { - u.showPulls(act.openPR) - } + u.openPR(act.openPR, act.openPRURL) if act.pin != "" && u.model.setPinned(act.pin, act.pinTo) { u.model.invalidate() id, pinned := act.pin, act.pinTo diff --git a/native/cmd/lich-native/pulls_actions.go b/native/cmd/lich-native/pulls_actions.go index 578b010e..a5129878 100644 --- a/native/cmd/lich-native/pulls_actions.go +++ b/native/cmd/lich-native/pulls_actions.go @@ -55,6 +55,30 @@ func (u *window) showPulls(number int) { u.model.invalidate() } +// openPR opens a card's pull request where prTarget sent it: number on the +// Pulls screen, else url in the browser; neither set opens nothing. +func (u *window) openPR(number int, url string) { + switch { + case number != 0: + u.showPulls(number) + case url != "": + go func() { u.logErr("open pull request", u.client.OpenExternal(u.ctx, url)) }() + } +} + +// prTarget is where a click on a card's pull request goes. The badge reads the +// pull request where the shell is (shown); the Pulls screen lists the +// project's, so a shell that left the session's checkout for another +// repository has one whose number names something else there, and it opens +// in the browser instead. Another worktree of the same project goes there too: +// a cleaned path cannot tell it from another repository. +func prTarget(pr *backend.PullRequest, shown, checkout string) (number int, url string) { + if under(checkout, shown) { + return pr.Number, "" + } + return 0, pr.URL +} + // checkoutPR is the active session's branch and its open PR, the project's // own checkout's without a session. func (u *window) checkoutPR(p backend.Project) (string, int) { diff --git a/native/cmd/lich-native/sidebar.go b/native/cmd/lich-native/sidebar.go index f5420e46..56eb8840 100644 --- a/native/cmd/lich-native/sidebar.go +++ b/native/cmd/lich-native/sidebar.go @@ -122,7 +122,9 @@ type sidebarAction struct { pin string // session id pinTo bool // the pin the click asked for openPR int // a pull request to show on the Pulls screen - launch launchRequest + // openPRURL is a pull request to open in the browser instead (prTarget). + openPRURL string + launch launchRequest // Open in, from the card menu: a directory for a shell session, the // editor or the file manager. terminalAt, editorAt, folderAt string @@ -192,6 +194,7 @@ func (a *sidebarAction) takeCard(c sidebarAction) { if c.openPR != 0 { a.openPR = c.openPR } + pick(&a.openPRURL, c.openPRURL) pick(&a.terminalAt, c.terminalAt) pick(&a.editorAt, c.editorAt) pick(&a.folderAt, c.folderAt) diff --git a/native/cmd/lich-native/sidebar_card.go b/native/cmd/lich-native/sidebar_card.go index 163aab09..ff4bfa4e 100644 --- a/native/cmd/lich-native/sidebar_card.go +++ b/native/cmd/lich-native/sidebar_card.go @@ -43,7 +43,8 @@ func (s *sidebar) card(gtx C, th *ui.Theme, sv *sessionView, active bool, folder } prChip := clickable(s.prChips, sv.ID) if prChip.Clicked(gtx) && sv.pr != nil { - act.activate, act.openPR = sv.ID, sv.pr.Number + act.activate = sv.ID + act.openPR, act.openPRURL = prTarget(sv.pr, sv.shown, sv.path) } fill := cardFill(th, sv.Color, active, click.Hovered()) th = th.On(fill)