Skip to content
Open
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
8 changes: 4 additions & 4 deletions GUI/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def on_open_feed_event(self, event):
self._open_feed_issue(owner, repo_name, number)
return

elif feed_event.type in ("PullRequestEvent", "PullRequestReviewEvent", "PullRequestReviewCommentEvent"):
elif feed_event.type in ("PullRequestEvent", "PullRequestReviewEvent", "PullRequestReviewCommentEvent", "PullRequestReviewThreadEvent"):
pr = payload.get("pull_request", {})
number = pr.get("number")
if number:
Expand All @@ -468,15 +468,15 @@ def on_open_feed_event(self, event):
self._open_feed_discussion(owner, repo_name, number)
return

elif feed_event.type == "PushEvent":
elif feed_event.type in ("PushEvent", "CommitCommentEvent"):
self._open_feed_commits(owner, repo_name)
return

elif feed_event.type == "ReleaseEvent":
self._open_feed_releases(owner, repo_name)
return

elif feed_event.type in ("WatchEvent", "ForkEvent", "CreateEvent", "PublicEvent"):
elif feed_event.type in ("WatchEvent", "ForkEvent", "CreateEvent", "PublicEvent", "GollumEvent", "MemberEvent"):
# For repo-level events, show the repo dialog
self._open_feed_repo_direct(owner, repo_name)
return
Expand Down Expand Up @@ -934,7 +934,7 @@ def _render_feed_list(self):

def _extract_feed_pr_key(self, event):
"""Extract a normalized key for PR-related feed events."""
if event.type not in ("PullRequestEvent", "PullRequestReviewEvent", "PullRequestReviewCommentEvent"):
if event.type not in ("PullRequestEvent", "PullRequestReviewEvent", "PullRequestReviewCommentEvent", "PullRequestReviewThreadEvent"):
return None

pr = event.payload.get("pull_request", {}) or {}
Expand Down
3 changes: 2 additions & 1 deletion GUI/pullrequests.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,8 @@ def update_comments(self, comments):
for comment in comments:
time_str = comment.created_at.strftime("%Y-%m-%d %H:%M") if comment.created_at else "Unknown"
preview = comment.body[:50].replace("\n", " ") + "..." if len(comment.body) > 50 else comment.body.replace("\n", " ")
self.comments_list.Append(f"{comment.user.login} ({time_str}): {preview}")
source = "[Review] " if comment.kind == "review" else ""
self.comments_list.Append(f"{comment.user.login} ({time_str}): {source}{preview}")

def on_comment_select(self, event):
"""Show selected comment content."""
Expand Down
42 changes: 40 additions & 2 deletions github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,9 +737,47 @@ def close_pull_request(self, owner: str, repo: str, number: int) -> bool:
result = self.update_pull_request(owner, repo, number, state="closed")
return result is not None

def get_pr_review_comments(self, owner: str, repo: str, number: int, per_page: int = 100) -> list[Comment]:
"""Get review comments on a pull request."""
comments = []
page = 1

while True:
response = self._session.get(
f"{GITHUB_API_URL}/repos/{owner}/{repo}/pulls/{number}/comments",
params={
"per_page": per_page,
"page": page
}
)

if response.status_code != 200:
break

data = response.json()
if not data:
break

for item in data:
comments.append(Comment.from_github_api(item, kind="review"))

if len(data) < per_page:
break

page += 1

return comments

def get_pr_comments(self, owner: str, repo: str, number: int, per_page: int = 100) -> list[Comment]:
"""Get comments on a pull request (issue comments, not review comments)."""
return self.get_issue_comments(owner, repo, number, per_page)
"""Get all comments on a pull request, including review comments."""
issue_comments = self.get_issue_comments(owner, repo, number, per_page)
review_comments = self.get_pr_review_comments(owner, repo, number, per_page)
comments = issue_comments + review_comments

# Keep stable ordering by creation time so the dialog mirrors issue behavior.
comments.sort(key=lambda c: c.created_at.timestamp() if c.created_at else 0)

return comments

def create_pr_comment(self, owner: str, repo: str, number: int, body: str) -> Comment | None:
"""Create a comment on a pull request."""
Expand Down
35 changes: 34 additions & 1 deletion models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Event:
"PublicEvent": "made public",
"PullRequestEvent": "pull request",
"PullRequestReviewEvent": "reviewed PR",
"PullRequestReviewThreadEvent": "updated PR review thread",
"PullRequestReviewCommentEvent": "commented on PR review",
"PushEvent": "pushed",
"ReleaseEvent": "released",
Expand Down Expand Up @@ -395,6 +396,18 @@ def get_action_description(self) -> str:
return f"commented on PR #{number}: {title}"
return f"commented on PR #{number}"

elif self.type == "PullRequestReviewThreadEvent":
action = payload.get("action", "")
pr = payload.get("pull_request", {})
number = pr.get("number", "")
title = pr.get("title", "")[:50]
title_suffix = f": {title}" if title else ""
if action == "resolved":
return f"resolved review thread on PR #{number}{title_suffix}"
elif action == "unresolved":
return f"unresolved review thread on PR #{number}{title_suffix}"
return f"{action} review thread on PR #{number}{title_suffix}"

elif self.type == "ReleaseEvent":
action = payload.get("action", "")
release = payload.get("release", {})
Expand Down Expand Up @@ -486,7 +499,27 @@ def get_web_url(self) -> str:
if number:
return f"{base_url}/pull/{number}"

elif self.type == "PullRequestReviewEvent" or self.type == "PullRequestReviewCommentEvent":
elif self.type == "PullRequestReviewCommentEvent":
comment = self.payload.get("comment", {})
html_url = comment.get("html_url")
if html_url:
return html_url
pr = self.payload.get("pull_request", {})
number = pr.get("number")
if number:
return f"{base_url}/pull/{number}"

elif self.type == "PullRequestReviewThreadEvent":
thread = self.payload.get("thread", {})
html_url = thread.get("html_url")
if html_url:
return html_url
pr = self.payload.get("pull_request", {})
number = pr.get("number")
if number:
return f"{base_url}/pull/{number}"

elif self.type == "PullRequestReviewEvent":
pr = self.payload.get("pull_request", {})
number = pr.get("number")
if number:
Expand Down
6 changes: 4 additions & 2 deletions models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ class Comment:
created_at: Optional[datetime]
updated_at: Optional[datetime]
html_url: str = ""
kind: str = "issue"

@classmethod
def from_github_api(cls, data: dict) -> 'Comment':
def from_github_api(cls, data: dict, kind: str = "issue") -> 'Comment':
created_at = None
if data.get('created_at'):
try:
Expand All @@ -71,7 +72,8 @@ def from_github_api(cls, data: dict) -> 'Comment':
user=User.from_github_api(data.get('user')),
created_at=created_at,
updated_at=updated_at,
html_url=data.get('html_url', '')
html_url=data.get('html_url', ''),
kind=kind
)


Expand Down
103 changes: 103 additions & 0 deletions tests/test_feed_pr_review_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from models.event import Event


def _make_event(event_type: str, payload: dict) -> Event:
return Event.from_api(
{
"id": "evt-1",
"type": event_type,
"actor": {"id": 1, "login": "alice", "avatar_url": ""},
"repo": {"id": 10, "name": "owner/repo", "url": ""},
"payload": payload,
"public": True,
"created_at": "2026-02-16T11:15:00Z",
}
)


# ── PullRequestReviewThreadEvent: action descriptions ──────────────────────

def test_pr_review_thread_resolved_with_title():
event = _make_event(
"PullRequestReviewThreadEvent",
{"action": "resolved", "pull_request": {"number": 42, "title": "Fix the thing"}},
)
assert event.get_action_description() == "resolved review thread on PR #42: Fix the thing"


def test_pr_review_thread_unresolved_with_title():
event = _make_event(
"PullRequestReviewThreadEvent",
{"action": "unresolved", "pull_request": {"number": 42, "title": "Fix the thing"}},
)
assert event.get_action_description() == "unresolved review thread on PR #42: Fix the thing"


def test_pr_review_thread_resolved_no_title():
event = _make_event(
"PullRequestReviewThreadEvent",
{"action": "resolved", "pull_request": {"number": 7, "title": ""}},
)
assert event.get_action_description() == "resolved review thread on PR #7"


def test_pr_review_thread_unknown_action_falls_back():
event = _make_event(
"PullRequestReviewThreadEvent",
{"action": "locked", "pull_request": {"number": 99, "title": "Something"}},
)
assert event.get_action_description() == "locked review thread on PR #99: Something"


def test_pr_review_thread_in_event_types_dict():
assert "PullRequestReviewThreadEvent" in Event.EVENT_TYPES


# ── PullRequestReviewThreadEvent: web URLs ─────────────────────────────────

def test_pr_review_thread_web_url_uses_thread_html_url():
event = _make_event(
"PullRequestReviewThreadEvent",
{
"action": "resolved",
"thread": {"html_url": "https://github.com/owner/repo/pull/42#discussion_r123"},
"pull_request": {"number": 42},
},
)
assert event.get_web_url() == "https://github.com/owner/repo/pull/42#discussion_r123"


def test_pr_review_thread_web_url_falls_back_to_pr_number():
event = _make_event(
"PullRequestReviewThreadEvent",
{
"action": "resolved",
"thread": {},
"pull_request": {"number": 42},
},
)
assert event.get_web_url() == "https://github.com/owner/repo/pull/42"


# ── PullRequestReviewCommentEvent: web URL uses comment html_url ───────────

def test_pr_review_comment_web_url_uses_comment_html_url():
event = _make_event(
"PullRequestReviewCommentEvent",
{
"comment": {"html_url": "https://github.com/owner/repo/pull/10#pullrequestreview-456"},
"pull_request": {"number": 10},
},
)
assert event.get_web_url() == "https://github.com/owner/repo/pull/10#pullrequestreview-456"


def test_pr_review_comment_web_url_falls_back_to_pr_number():
event = _make_event(
"PullRequestReviewCommentEvent",
{
"comment": {},
"pull_request": {"number": 10},
},
)
assert event.get_web_url() == "https://github.com/owner/repo/pull/10"