Skip to content

Commit f8b3b03

Browse files
brovattenclaude
andcommitted
feat: merge the PR-comment browser + editor CTAs into one line
Replace the two stacked CTA lines (a '🌐 Explore … in your browser β†’' line and a separate 'See this … in your editor: Open in VS Code β†’' line) with a single sentence: 'Explore this PR's architecture in your browser or VS Code.' The browser link only joins when webview_ready (head analysis committed, non-fork PR); 'your' rides with the browser entry alone so it still reads right when only the editor link is present ('… in VS Code'). Multiple editors render with an Oxford 'or' ('browser, VS Code, or Cursor'). The ⚠️ health banner and the proxy-only 'Get the extension' line stay separate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 290e36b commit f8b3b03

2 files changed

Lines changed: 114 additions & 57 deletions

File tree

β€Žscripts/build_cta.pyβ€Ž

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
"""Build the call-to-action footer appended to the architecture-diff PR comment.
22
3-
The footer drives to the VS Code/Cursor **extension** with an editor link, and a
4-
warning banner when real health findings exist. With a click proxy (``cta_base``)
5-
the links route through it (owner/repo/pr tracked) and can deep-link into the
6-
editor (the proxy redirects to a ``vscode:``/``cursor:`` URL) plus a separate
7-
"install the extension" link. Without a proxy GitHub's comment sanitizer strips
8-
custom ``vscode:``/``cursor:`` schemes β€” a deep link would render as dead text β€”
9-
so the editor link points at the extension's plain-https listing instead (VS Code
10-
Marketplace, Cursor via Open VSX), which is the only clickable option. A no-install
11-
hosted-webview ("explore in browser") line is added when ``webview_ready`` β€” i.e. the
12-
head ``analysis.json`` was committed to the PR branch and this isn't a fork PR β€” so
13-
the webview can fetch a committed analysis at the head SHA (see docs/COMMIT_STRATEGY.md).
3+
The body is a single line β€” "Explore this PR's architecture in your browser or
4+
VS Code" β€” that merges the hosted-webview link with the editor link(s), preceded by
5+
a warning banner when real health findings exist. The "browser" link (a no-install
6+
hosted webview) is included only when ``webview_ready`` β€” i.e. the head
7+
``analysis.json`` was committed to the PR branch and this isn't a fork PR β€” so the
8+
webview can fetch a committed analysis at the head SHA (see docs/COMMIT_STRATEGY.md);
9+
otherwise the line is just the editor link(s). With a click proxy (``cta_base``) the
10+
editor link routes through it (owner/repo/pr tracked) and deep-links into the editor
11+
(the proxy redirects to a ``vscode:``/``cursor:`` URL), and a separate "install the
12+
extension" link is appended. Without a proxy GitHub's comment sanitizer strips custom
13+
``vscode:``/``cursor:`` schemes β€” a deep link would render as dead text β€” so the editor
14+
link points at the extension's plain-https listing instead (VS Code Marketplace, Cursor
15+
via Open VSX), which is the only clickable option.
1416
1517
Editor coverage is deliberately limited to **VS Code and Cursor**. Per the 2025
1618
Stack Overflow Developer Survey (https://survey.stackoverflow.co/2025/technology/),
@@ -59,22 +61,31 @@ def detect_editors(repo_path: Path) -> list[str]:
5961
}
6062

6163

62-
def build_webview_link(webview_base: str, owner: str, repo: str, head_sha: str, base_sha: str) -> str | None:
63-
"""Return the markdown "explore in browser" line, or None if not buildable.
64+
def webview_url(webview_base: str, owner: str, repo: str, head_sha: str, base_sha: str) -> str | None:
65+
"""Return the hosted-webview deep-link URL for this PR's head-vs-base diff, or None.
6466
65-
Deep-links the hosted webview straight to this PR's head-vs-base architecture
66-
diff: ``?repo=owner/repo&ref=<head_sha>&compare=<base_sha>``. Pinned to exact
67-
SHAs so the committed ``analysis.json`` the webview fetches matches this run. For
68-
a private repo the webview itself sends the viewer through GitHub sign-in and then
69-
loads the same diff. Returns None when the base/head pieces aren't all present.
67+
Deep-links straight to the diff: ``?repo=owner/repo&ref=<head_sha>&compare=<base_sha>``.
68+
Pinned to exact SHAs so the committed ``analysis.json`` the webview fetches matches
69+
this run. For a private repo the webview itself sends the viewer through GitHub
70+
sign-in and then loads the same diff. Returns None when the base/head pieces aren't
71+
all present.
7072
"""
7173
if not (webview_base and owner and repo and head_sha):
7274
return None
7375
base = webview_base.rstrip("/")
7476
params = {"repo": f"{owner}/{repo}", "ref": head_sha}
7577
if base_sha:
7678
params["compare"] = base_sha
77-
return f"🌐 [**Explore this PR’s architecture in your browser β†’**]({base}/?{urlencode(params)})"
79+
return f"{base}/?{urlencode(params)}"
80+
81+
82+
def _join_or(items: list[str]) -> str:
83+
"""Join with commas and a trailing 'or': 'a' / 'a or b' / 'a, b, or c'."""
84+
if len(items) <= 1:
85+
return items[0] if items else ""
86+
if len(items) == 2:
87+
return f"{items[0]} or {items[1]}"
88+
return ", ".join(items[:-1]) + f", or {items[-1]}"
7889

7990

8091
def build_cta(
@@ -108,11 +119,6 @@ def build_cta(
108119
noun = "issue" if issues == 1 else "issues"
109120
parts.append(f"⚠️ **{issues} architecture {noun} found** β€” open CodeBoarding to explore them.")
110121

111-
if webview_ready:
112-
webview_line = build_webview_link(webview_base, owner, repo, head_sha, base_sha)
113-
if webview_line:
114-
parts.append(webview_line)
115-
116122
editors = detect_editors(repo_path)
117123
if cta_base:
118124
base = cta_base.rstrip("/")
@@ -126,8 +132,18 @@ def link(path: str, **extra: str) -> str:
126132
editor_href = {e: _EDITOR_MARKETPLACE[e] for e in editors}
127133
extension_href = None
128134

129-
editor_links = " Β· ".join(f"[**Open in {_EDITOR_LABEL[e]} β†’**]({editor_href[e]})" for e in editors)
130-
parts.append(f"See this architecture in your editor: {editor_links}")
135+
# One line that merges the hosted-webview "browser" link β€” only when
136+
# ``webview_ready`` (the head analysis was committed and this isn't a fork PR) β€”
137+
# with the editor link(s), which always render. "your" rides with the browser
138+
# entry alone, so the sentence reads naturally with or without it:
139+
# "in your browser or VS Code" / "in VS Code".
140+
targets: list[str] = []
141+
if webview_ready:
142+
wv = webview_url(webview_base, owner, repo, head_sha, base_sha)
143+
if wv:
144+
targets.append(f"your [**browser**]({wv})")
145+
targets += [f"[**{_EDITOR_LABEL[e]}**]({editor_href[e]})" for e in editors]
146+
parts.append(f"Explore this PR’s architecture in {_join_or(targets)}.")
131147
if extension_href:
132148
parts.append(f"πŸ’‘ New to CodeBoarding? [**Get the extension β†’**]({extension_href})")
133149

β€Žtests/test_build_cta.pyβ€Ž

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ def test_no_proxy_links_editor_to_https_listing_no_get_extension(self):
3535
out = bc.build_cta("", "o", "r", "1", repo_with(".cursor"), issues=3)
3636
self.assertIn("3 architecture issues found", out)
3737
# Cursor -> Open VSX https listing. A cursor: scheme would be stripped by GitHub.
38-
self.assertIn("[**Open in Cursor β†’**](https://open-vsx.org/extension/CodeBoarding/codeboarding)", out)
38+
self.assertIn("[**Cursor**](https://open-vsx.org/extension/CodeBoarding/codeboarding)", out)
3939
self.assertNotIn("cursor:extension", out)
4040
self.assertNotIn("Get the extension", out) # dropped without a proxy
41-
self.assertNotIn("Open in VS Code", out) # cursor-only repo
41+
self.assertNotIn("VS Code", out) # cursor-only repo
4242

4343
def test_no_proxy_vscode_marketplace_https_no_banner_at_zero(self):
4444
out = bc.build_cta("", "o", "r", "1", repo_with()) # neither dir, no issues
4545
self.assertIn(
46-
"[**Open in VS Code β†’**](https://marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding)",
46+
"[**VS Code**](https://marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding)",
4747
out,
4848
)
4949
self.assertNotIn("vscode:extension", out) # custom scheme stripped by GitHub
@@ -53,22 +53,21 @@ def test_no_proxy_vscode_marketplace_https_no_banner_at_zero(self):
5353
def test_links_banner_and_cursor_only(self):
5454
out = bc.build_cta("https://x.dev/", "Org", "Repo", "9", repo_with(".cursor"), issues=2)
5555
self.assertIn("2 architecture issues found", out)
56-
self.assertNotIn("use-workspace", out) # webview/browser tier deferred β€” extension-direct
5756
self.assertIn("open-in-editor?owner=Org&repo=Repo&pr=9&editor=cursor", out)
58-
self.assertIn("use-marketplace?owner=Org&repo=Repo&pr=9", out)
59-
self.assertNotIn("Open in VS Code", out) # cursor-only repo
57+
self.assertIn("use-marketplace?owner=Org&repo=Repo&pr=9", out) # proxy "Get the extension"
58+
self.assertNotIn("VS Code", out) # cursor-only repo
6059

6160
def test_no_banner_when_zero_issues_and_default_vscode(self):
6261
out = bc.build_cta("https://x.dev", "o", "r", "1", repo_with(), issues=0)
6362
self.assertNotIn("architecture issue", out)
64-
self.assertIn("Open in VS Code", out)
65-
self.assertNotIn("Open in Cursor", out)
63+
self.assertIn("VS Code", out)
64+
self.assertNotIn("Cursor", out)
6665

6766
def test_both_editors_singular_issue(self):
6867
out = bc.build_cta("https://x.dev", "o", "r", "1", repo_with(".vscode", ".cursor"), issues=1)
6968
self.assertIn("1 architecture issue found", out) # singular
70-
self.assertIn("Open in VS Code", out)
71-
self.assertIn("Open in Cursor", out)
69+
self.assertIn("VS Code", out)
70+
self.assertIn("Cursor", out)
7271

7372
def test_trailing_slash_in_base_is_normalized(self):
7473
a = bc.build_cta("https://x.dev/", "o", "r", "1", repo_with())
@@ -77,26 +76,27 @@ def test_trailing_slash_in_base_is_normalized(self):
7776
self.assertEqual(a, b)
7877

7978

80-
class TestWebviewLink(unittest.TestCase):
79+
class TestWebviewUrl(unittest.TestCase):
8180
WV = "https://app.codeboarding.org"
8281

83-
def test_link_built_with_head_ref_and_compare_base(self):
84-
link = bc.build_webview_link(self.WV, "Org", "Repo", "headsha", "basesha")
85-
self.assertIn("https://app.codeboarding.org/?", link)
86-
self.assertIn("repo=Org%2FRepo", link)
87-
self.assertIn("ref=headsha", link)
88-
self.assertIn("compare=basesha", link)
82+
def test_url_built_with_head_ref_and_compare_base(self):
83+
url = bc.webview_url(self.WV, "Org", "Repo", "headsha", "basesha")
84+
self.assertIn("https://app.codeboarding.org/?", url)
85+
self.assertIn("repo=Org%2FRepo", url)
86+
self.assertIn("ref=headsha", url)
87+
self.assertIn("compare=basesha", url)
88+
self.assertFalse(url.startswith("🌐")) # a bare URL now, not a markdown line
8989

90-
def test_link_omits_compare_when_no_base(self):
91-
link = bc.build_webview_link(self.WV, "o", "r", "headsha", "")
92-
self.assertIn("ref=headsha", link)
93-
self.assertNotIn("compare=", link)
90+
def test_url_omits_compare_when_no_base(self):
91+
url = bc.webview_url(self.WV, "o", "r", "headsha", "")
92+
self.assertIn("ref=headsha", url)
93+
self.assertNotIn("compare=", url)
9494

95-
def test_link_none_without_head_sha_or_base(self):
96-
self.assertIsNone(bc.build_webview_link(self.WV, "o", "r", "", "basesha"))
97-
self.assertIsNone(bc.build_webview_link("", "o", "r", "headsha", "basesha"))
95+
def test_url_none_without_head_sha_or_base(self):
96+
self.assertIsNone(bc.webview_url(self.WV, "o", "r", "", "basesha"))
97+
self.assertIsNone(bc.webview_url("", "o", "r", "headsha", "basesha"))
9898

99-
def test_cta_emits_webview_line_when_ready(self):
99+
def test_cta_includes_browser_link_when_ready(self):
100100
out = bc.build_cta(
101101
"",
102102
"Org",
@@ -110,10 +110,12 @@ def test_cta_emits_webview_line_when_ready(self):
110110
webview_ready=True,
111111
)
112112
self.assertIn("Explore this PR", out)
113+
self.assertIn("your [**browser**](", out)
113114
self.assertIn("ref=headsha", out)
114115
self.assertIn("compare=basesha", out)
116+
self.assertIn("VS Code", out) # editor merged into the same line
115117

116-
def test_cta_omits_webview_line_when_not_ready(self):
118+
def test_cta_omits_browser_link_when_not_ready(self):
117119
# Fork PR / head analysis not committed -> webview can't fetch at head SHA.
118120
out = bc.build_cta(
119121
"",
@@ -127,11 +129,12 @@ def test_cta_omits_webview_line_when_not_ready(self):
127129
base_sha="basesha",
128130
webview_ready=False,
129131
)
130-
self.assertNotIn("Explore this PR", out)
131-
# Editor CTA is still present regardless.
132-
self.assertIn("Open in VS Code", out)
132+
self.assertNotIn("ref=headsha", out) # no browser link
133+
self.assertNotIn("[**browser**]", out)
134+
self.assertIn("Explore this PR", out) # the line is still there, editor-only
135+
self.assertIn("VS Code", out)
133136

134-
def test_cta_omits_webview_line_when_ready_but_no_base_url(self):
137+
def test_cta_omits_browser_link_when_ready_but_no_base_url(self):
135138
out = bc.build_cta(
136139
"",
137140
"Org",
@@ -144,7 +147,45 @@ def test_cta_omits_webview_line_when_ready_but_no_base_url(self):
144147
base_sha="basesha",
145148
webview_ready=True,
146149
)
147-
self.assertNotIn("Explore this PR", out)
150+
self.assertNotIn("[**browser**]", out)
151+
self.assertNotIn("ref=headsha", out)
152+
153+
154+
class TestJoinOr(unittest.TestCase):
155+
def test_join_shapes(self):
156+
self.assertEqual(bc._join_or(["a"]), "a")
157+
self.assertEqual(bc._join_or(["a", "b"]), "a or b")
158+
self.assertEqual(bc._join_or(["a", "b", "c"]), "a, b, or c")
159+
160+
161+
class TestMergedExploreLine(unittest.TestCase):
162+
WV = "https://app.codeboarding.org"
163+
164+
def _ready(self, repo, cta=""):
165+
return bc.build_cta(
166+
cta, "o", "r", "1", repo, webview_base=self.WV, head_sha="h", base_sha="b", webview_ready=True
167+
)
168+
169+
def test_browser_and_single_editor_joined_with_or(self):
170+
out = self._ready(repo_with()) # default VS Code
171+
self.assertIn("in your [**browser**](", out)
172+
self.assertIn(") or [**VS Code**](", out) # browser <or> editor on one line
173+
174+
def test_editor_only_has_no_your_and_no_browser(self):
175+
out = bc.build_cta("", "o", "r", "1", repo_with()) # no webview
176+
self.assertIn("architecture in [**VS Code**](", out) # "in <editor>" with no "your"
177+
self.assertNotIn("browser", out)
178+
179+
def test_browser_and_two_editors_use_oxford_or(self):
180+
out = self._ready(repo_with(".vscode", ".cursor"))
181+
self.assertIn("your [**browser**](", out)
182+
self.assertIn(", or [**Cursor**](", out) # 3 targets -> ", or" before the last
183+
184+
def test_two_editors_no_browser_joined_with_or(self):
185+
out = bc.build_cta("", "o", "r", "1", repo_with(".vscode", ".cursor"))
186+
self.assertIn(" or [**Cursor**](", out)
187+
self.assertNotIn(", or [**Cursor**]", out) # 2 targets -> plain "or", no Oxford comma
188+
self.assertNotIn("browser", out)
148189

149190

150191
if __name__ == "__main__":

0 commit comments

Comments
Β (0)