diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDOAgent47.page1.json b/fixtures/github/issues.pull_request_timeline.PR_kwDOAgent47.page1.json index 33e71ee..99af6dd 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDOAgent47.page1.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDOAgent47.page1.json @@ -51,6 +51,14 @@ "messageHeadline": "Split prompt packing from worker execution" } }, + { + "__typename": "PullRequestCommit", + "commit": { + "committedDate": "2026-05-01T03:12:00Z", + "abbreviatedOid": "b71c44e", + "messageHeadline": "Add worker context envelope validation" + } + }, { "__typename": "IssueComment", "createdAt": "2026-05-01T03:20:00Z", @@ -61,6 +69,16 @@ }, "body": "Let us keep this in draft until telemetry lands." }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T03:45:00Z", + "author": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + }, + "body": "Telemetry hooks are now wired through the worker boundary." + }, { "__typename": "ConvertToDraftEvent", "createdAt": "2026-05-01T04:00:00Z", @@ -94,6 +112,35 @@ }, "state": "COMMENTED", "body": "Split looks good; leaving draft until telemetry is in." + }, + { + "__typename": "ReadyForReviewEvent", + "createdAt": "2026-05-02T01:10:00Z", + "actor": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + } + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T01:25:00Z", + "author": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + }, + "body": "Telemetry is now visible in the worker trace panel." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T01:40:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "body": "The context envelope boundary is clear now; ready for final review." } ] } diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDODesign31.page1.json b/fixtures/github/issues.pull_request_timeline.PR_kwDODesign31.page1.json index c9609e7..ed0086b 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDODesign31.page1.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDODesign31.page1.json @@ -21,6 +21,14 @@ "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" } }, + { + "__typename": "PullRequestCommit", + "commit": { + "committedDate": "2026-05-02T12:20:00Z", + "abbreviatedOid": "8ad14c3", + "messageHeadline": "Tighten dashboard spacing token scale" + } + }, { "__typename": "PullRequestReview", "createdAt": "2026-05-02T15:40:00Z", @@ -32,6 +40,16 @@ "state": "CHANGES_REQUESTED", "body": "The 12px sidebar gutter still feels cramped." }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T15:55:00Z", + "author": { + "__typename": "User", + "login": "mariahops", + "avatarUrl": "https://avatars.githubusercontent.com/u/6161?v=4" + }, + "body": "I will widen the sidebar gutter and rebalance the compact row padding." + }, { "__typename": "BaseRefChangedEvent", "createdAt": "2026-05-02T18:05:00Z", @@ -51,6 +69,37 @@ }, "body": "Updated the spacing tokens to align with the latest mock." }, + { + "__typename": "PullRequestReview", + "createdAt": "2026-05-02T18:45:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "state": "APPROVED", + "body": "Spacing reads better now; compact dashboard surfaces still scan well." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T19:05:00Z", + "author": { + "__typename": "User", + "login": "mariahops", + "avatarUrl": "https://avatars.githubusercontent.com/u/6161?v=4" + }, + "body": "I updated the visual comparison screenshot with the final spacing tokens." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T19:30:00Z", + "author": { + "__typename": "Bot", + "login": "design-bot", + "avatarUrl": "https://avatars.githubusercontent.com/in/97531?v=4" + }, + "body": "Design token snapshot updated for dashboard spacing review." + }, { "__typename": "AutoMergeEnabledEvent", "createdAt": "2026-05-03T09:00:00Z", diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDOInfra19.page1.json b/fixtures/github/issues.pull_request_timeline.PR_kwDOInfra19.page1.json index f457c06..55a74bf 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDOInfra19.page1.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDOInfra19.page1.json @@ -26,6 +26,14 @@ "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" } }, + { + "__typename": "PullRequestCommit", + "commit": { + "committedDate": "2026-04-30T07:05:00Z", + "abbreviatedOid": "6fd2a90", + "messageHeadline": "Document standby promotion rollback step" + } + }, { "__typename": "ReopenedEvent", "createdAt": "2026-05-01T09:45:00Z", @@ -35,6 +43,37 @@ "avatarUrl": "https://avatars.githubusercontent.com/u/8181?v=4" } }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T10:05:00Z", + "author": { + "__typename": "User", + "login": "piperlane", + "avatarUrl": "https://avatars.githubusercontent.com/u/8181?v=4" + }, + "body": "Reopened after confirming the rollback path with infra." + }, + { + "__typename": "PullRequestReview", + "createdAt": "2026-05-02T11:30:00Z", + "author": { + "__typename": "User", + "login": "piperlane", + "avatarUrl": "https://avatars.githubusercontent.com/u/8181?v=4" + }, + "state": "APPROVED", + "body": "Runbook steps are clear enough for the on-call handoff." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T11:45:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "body": "I added the approval note to the release handoff checklist." + }, { "__typename": "ClosedEvent", "createdAt": "2026-05-02T12:05:00Z", @@ -43,6 +82,16 @@ "login": "piperlane", "avatarUrl": "https://avatars.githubusercontent.com/u/8181?v=4" } + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-02T12:20:00Z", + "author": { + "__typename": "Bot", + "login": "infra-bot", + "avatarUrl": "https://avatars.githubusercontent.com/in/13579?v=4" + }, + "body": "Runbook closure recorded for the standby promotion playbook." } ] } diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDONovem84.page1.json b/fixtures/github/issues.pull_request_timeline.PR_kwDONovem84.page1.json index 3070b3f..c2d623f 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDONovem84.page1.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDONovem84.page1.json @@ -29,6 +29,25 @@ "messageHeadline": "Hydrate issue pane from cached dashboard state" } }, + { + "__typename": "PullRequestCommit", + "commit": { + "committedDate": "2026-05-01T10:45:00Z", + "abbreviatedOid": "73ac918", + "messageHeadline": "Preserve selected pull request while refreshing issues" + } + }, + { + "__typename": "PullRequestReview", + "createdAt": "2026-05-01T11:30:00Z", + "author": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + }, + "state": "APPROVED", + "body": "Query cache behavior looks stable during rapid list refreshes." + }, { "__typename": "CrossReferencedEvent", "createdAt": "2026-05-01T12:00:00Z", @@ -61,6 +80,36 @@ "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" }, "body": "Cache hydration is now stable across refetches." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T12:45:00Z", + "author": { + "__typename": "Bot", + "login": "novem-ci", + "avatarUrl": "https://avatars.githubusercontent.com/in/54321?v=4" + }, + "body": "Smoke suite passed with cached issue pane hydration enabled." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T13:10:00Z", + "author": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + }, + "body": "I also checked keyboard navigation during refresh and the selection anchor holds." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T13:25:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "body": "Great, I will mirror this behavior in the pull request detail pane next." } ] } diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDONovem85.page1.json b/fixtures/github/issues.pull_request_timeline.PR_kwDONovem85.page1.json index 0076465..52e61be 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDONovem85.page1.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDONovem85.page1.json @@ -38,6 +38,14 @@ "messageHeadline": "Cache repository list for titlebar context switcher" } }, + { + "__typename": "PullRequestCommit", + "commit": { + "committedDate": "2026-05-03T08:33:00Z", + "abbreviatedOid": "4e19b62", + "messageHeadline": "Reuse warm repository cache while picker refreshes" + } + }, { "__typename": "ReviewRequestedEvent", "createdAt": "2026-05-03T08:40:00Z", @@ -62,6 +70,47 @@ }, "body": "The cached picker feels much faster under repo churn." }, + { + "__typename": "PullRequestReview", + "createdAt": "2026-05-03T10:20:00Z", + "author": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + }, + "state": "COMMENTED", + "body": "One edge case remains when the repository disappears mid-refresh." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-03T11:15:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "body": "Added a fallback to keep the picker focused when the selected repo is removed." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-03T11:40:00Z", + "author": { + "__typename": "Bot", + "login": "novem-ci", + "avatarUrl": "https://avatars.githubusercontent.com/in/54321?v=4" + }, + "body": "Repository picker regression suite passed against the warm cache path." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-03T12:05:00Z", + "author": { + "__typename": "User", + "login": "leaferiksen", + "avatarUrl": "https://avatars.githubusercontent.com/u/5151?v=4" + }, + "body": "Confirmed the disappearing repository case no longer drops focus." + }, { "__typename": "AutoMergeEnabledEvent", "createdAt": "2026-05-04T06:30:00Z", diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page1.json b/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page1.json index c8238a5..aecb224 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page1.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page1.json @@ -29,6 +29,14 @@ "messageHeadline": "Add release handoff checklist panel" } }, + { + "__typename": "PullRequestCommit", + "commit": { + "committedDate": "2026-04-29T09:40:00Z", + "abbreviatedOid": "51d09ee", + "messageHeadline": "Add QA owner column to release checklist" + } + }, { "__typename": "IssueComment", "createdAt": "2026-04-29T11:00:00Z", @@ -39,6 +47,16 @@ }, "body": "Release checklist is ready for QA." }, + { + "__typename": "IssueComment", + "createdAt": "2026-04-29T11:35:00Z", + "author": { + "__typename": "User", + "login": "mariahops", + "avatarUrl": "https://avatars.githubusercontent.com/u/6161?v=4" + }, + "body": "QA handoff looks complete; I added sign-off notes to the release plan." + }, { "__typename": "ReviewRequestedEvent", "createdAt": "2026-04-30T09:20:00Z", @@ -75,6 +93,39 @@ "label": { "name": "release-blocker" } + }, + { + "__typename": "ReviewRequestedEvent", + "createdAt": "2026-05-01T04:12:00Z", + "actor": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "requestedReviewer": { + "__typename": "Team", + "name": "release-engineering" + } + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T04:14:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "body": "Release engineering is looped in for the final handoff validation." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T04:16:00Z", + "author": { + "__typename": "Bot", + "login": "release-bot", + "avatarUrl": "https://avatars.githubusercontent.com/in/24680?v=4" + }, + "body": "Release checklist validation started for sprint planner handoff." } ] } diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page2.json b/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page2.json index 354f1af..f899a64 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page2.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page2.json @@ -35,6 +35,16 @@ "abbreviatedOid": "be7a811" } }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T06:18:00Z", + "author": { + "__typename": "User", + "login": "rorycraft", + "avatarUrl": "https://avatars.githubusercontent.com/u/7171?v=4" + }, + "body": "Force-pushed to fold in the final checklist copy edits." + }, { "__typename": "MilestonedEvent", "createdAt": "2026-05-01T06:35:00Z", @@ -57,6 +67,47 @@ "name": "tests" } }, + { + "__typename": "PullRequestReview", + "createdAt": "2026-05-01T08:10:00Z", + "author": { + "__typename": "User", + "login": "mariahops", + "avatarUrl": "https://avatars.githubusercontent.com/u/6161?v=4" + }, + "state": "CHANGES_REQUESTED", + "body": "Please add the support rotation owner before this queues." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T09:25:00Z", + "author": { + "__typename": "User", + "login": "rorycraft", + "avatarUrl": "https://avatars.githubusercontent.com/u/7171?v=4" + }, + "body": "Support rotation owner is now included in the handoff table." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T10:00:00Z", + "author": { + "__typename": "User", + "login": "mariahops", + "avatarUrl": "https://avatars.githubusercontent.com/u/6161?v=4" + }, + "body": "That resolves my release blocker concern." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-01T11:30:00Z", + "author": { + "__typename": "Bot", + "login": "release-bot", + "avatarUrl": "https://avatars.githubusercontent.com/in/24680?v=4" + }, + "body": "Release checklist now includes QA, support, and rollout owner sign-offs." + }, { "__typename": "AutoMergeEnabledEvent", "createdAt": "2026-05-02T02:15:00Z", diff --git a/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page3.json b/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page3.json index 9490c27..eab8a48 100644 --- a/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page3.json +++ b/fixtures/github/issues.pull_request_timeline.PR_kwDOSprint62.page3.json @@ -43,6 +43,26 @@ "avatarUrl": "https://avatars.githubusercontent.com/u/7171?v=4" } }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-03T12:25:00Z", + "author": { + "__typename": "User", + "login": "rorycraft", + "avatarUrl": "https://avatars.githubusercontent.com/u/7171?v=4" + }, + "body": "Linked the rollout issue so release notes can track the dependency." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-03T12:35:00Z", + "author": { + "__typename": "User", + "login": "kennethnym", + "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" + }, + "body": "I dismissed the stale review so the final approval reflects the latest force-push." + }, { "__typename": "ReviewDismissedEvent", "createdAt": "2026-05-03T12:50:00Z", @@ -62,6 +82,17 @@ }, "milestoneTitle": "May Release" }, + { + "__typename": "PullRequestReview", + "createdAt": "2026-05-04T08:05:00Z", + "author": { + "__typename": "User", + "login": "mariahops", + "avatarUrl": "https://avatars.githubusercontent.com/u/6161?v=4" + }, + "state": "APPROVED", + "body": "Support rotation is documented and the release checklist is ready." + }, { "__typename": "MergedEvent", "createdAt": "2026-05-04T18:10:00Z", @@ -79,6 +110,26 @@ "login": "kennethnym", "avatarUrl": "https://avatars.githubusercontent.com/u/4242?v=4" } + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-04T18:25:00Z", + "author": { + "__typename": "Bot", + "login": "release-bot", + "avatarUrl": "https://avatars.githubusercontent.com/in/24680?v=4" + }, + "body": "Release handoff checklist was merged and the rollout issue has been updated." + }, + { + "__typename": "IssueComment", + "createdAt": "2026-05-04T18:40:00Z", + "author": { + "__typename": "User", + "login": "rorycraft", + "avatarUrl": "https://avatars.githubusercontent.com/u/7171?v=4" + }, + "body": "Thanks, I copied the final checklist into the release notes." } ] } diff --git a/src/api/issues.rs b/src/api/issues.rs index 5c5b1ec..6d06d67 100644 --- a/src/api/issues.rs +++ b/src/api/issues.rs @@ -66,122 +66,122 @@ pub(crate) struct PullRequestTimeline { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum PullRequestTimelineItem { Assigned { - created_at: String, + created_at: Arc, actor: Option, assignee: Option, }, Unassigned { - created_at: String, + created_at: Arc, actor: Option, assignee: Option, }, Comment { - created_at: String, + created_at: Arc, author: Option, - body: String, + body: Arc, }, Commit { - committed_at: String, - abbreviated_oid: String, - message_headline: String, + committed_at: Arc, + abbreviated_oid: Arc, + message_headline: Arc, }, Review { - created_at: String, + created_at: Arc, author: Option, - state: String, - body: String, + state: Arc, + body: Arc, }, ReviewRequested { - created_at: String, + created_at: Arc, actor: Option, reviewer: Option, }, ReviewRequestRemoved { - created_at: String, + created_at: Arc, actor: Option, reviewer: Option, }, ReviewDismissed { - created_at: String, + created_at: Arc, actor: Option, }, Merged { - created_at: String, + created_at: Arc, actor: Option, }, Closed { - created_at: String, + created_at: Arc, actor: Option, }, Reopened { - created_at: String, + created_at: Arc, actor: Option, }, ConvertToDraft { - created_at: String, + created_at: Arc, actor: Option, }, ReadyForReview { - created_at: String, + created_at: Arc, actor: Option, }, HeadRefForcePushed { - created_at: String, + created_at: Arc, actor: Option, - before_commit_oid: Option, - after_commit_oid: Option, + before_commit_oid: Option>, + after_commit_oid: Option>, }, BaseRefChanged { - created_at: String, + created_at: Arc, actor: Option, }, Labeled { - created_at: String, + created_at: Arc, actor: Option, - label: String, + label: Arc, }, Unlabeled { - created_at: String, + created_at: Arc, actor: Option, - label: String, + label: Arc, }, Milestoned { - created_at: String, + created_at: Arc, actor: Option, - milestone_title: String, + milestone_title: Arc, }, Demilestoned { - created_at: String, + created_at: Arc, actor: Option, - milestone_title: String, + milestone_title: Arc, }, Referenced { - created_at: String, + created_at: Arc, actor: Option, }, CrossReferenced { - created_at: String, + created_at: Arc, actor: Option, }, AutoMergeEnabled { - created_at: String, + created_at: Arc, actor: Option, }, AutoMergeDisabled { - created_at: String, + created_at: Arc, actor: Option, - reason: String, + reason: Arc, }, AddedToMergeQueue { - created_at: String, + created_at: Arc, actor: Option, }, RemovedFromMergeQueue { - created_at: String, + created_at: Arc, actor: Option, }, Other { - typename: String, + typename: Arc, }, } @@ -215,9 +215,9 @@ pub(crate) enum ChangeType { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct TimelineActor { - pub(crate) kind: String, - pub(crate) name: String, - pub(crate) avatar_url: Option, + pub(crate) kind: Arc, + pub(crate) name: Arc, + pub(crate) avatar_url: Option>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] @@ -583,8 +583,8 @@ impl query::QueryFn for FetchPullRequestTimeline { | actorFieldsOn::User => "User", } .into(), - name: login, - avatar_url: Some(avatar_url), + name: login.into(), + avatar_url: Some(avatar_url.into()), } } @@ -592,23 +592,23 @@ impl query::QueryFn for FetchPullRequestTimeline { match actor { | assigneeFields::Bot(actor) => TimelineActor { kind: "Bot".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, | assigneeFields::Mannequin(actor) => TimelineActor { kind: "Mannequin".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, | assigneeFields::Organization(actor) => TimelineActor { kind: "Organization".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, | assigneeFields::User(actor) => TimelineActor { kind: "User".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, } } @@ -617,28 +617,28 @@ impl query::QueryFn for FetchPullRequestTimeline { match actor { | requestedReviewerFields::Bot(actor) => TimelineActor { kind: "Bot".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, | requestedReviewerFields::Mannequin(actor) => TimelineActor { kind: "Mannequin".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, | requestedReviewerFields::Team(actor) => TimelineActor { kind: "Team".into(), - name: actor.name, + name: actor.name.into(), avatar_url: None, }, | requestedReviewerFields::User(actor) => TimelineActor { kind: "User".into(), - name: actor.login, - avatar_url: Some(actor.avatar_url), + name: actor.login.into(), + avatar_url: Some(actor.avatar_url.into()), }, } } - fn normalize_review_state(state: PullRequestReviewState) -> String { + fn normalize_review_state(state: PullRequestReviewState) -> Arc { match state { | PullRequestReviewState::PENDING => "PENDING", | PullRequestReviewState::COMMENTED => "COMMENTED", @@ -657,166 +657,168 @@ impl query::QueryFn for FetchPullRequestTimeline { PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AssignedEvent( event, ) => PullRequestTimelineItem::Assigned { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), assignee: event.assignee.map(normalize_assignee), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::UnassignedEvent( event, ) => PullRequestTimelineItem::Unassigned { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), assignee: event.assignee.map(normalize_assignee), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::IssueComment( event, ) => PullRequestTimelineItem::Comment { - created_at: event.created_at, + created_at: event.created_at.into(), author: event.author.map(normalize_actor), - body: event.body, + body: event.body.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::PullRequestCommit( event, ) => PullRequestTimelineItem::Commit { - committed_at: event.commit.committed_date, - abbreviated_oid: event.commit.abbreviated_oid, - message_headline: event.commit.message_headline, + committed_at: event.commit.committed_date.into(), + abbreviated_oid: event.commit.abbreviated_oid.into(), + message_headline: event.commit.message_headline.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::PullRequestReview( event, ) => PullRequestTimelineItem::Review { - created_at: event.created_at, + created_at: event.created_at.into(), author: event.author.map(normalize_actor), state: normalize_review_state(event.state), - body: event.body, + body: event.body.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReviewRequestedEvent( event, ) => PullRequestTimelineItem::ReviewRequested { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), reviewer: event.requested_reviewer.map(normalize_requested_reviewer), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReviewRequestRemovedEvent( event, ) => PullRequestTimelineItem::ReviewRequestRemoved { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), reviewer: event.requested_reviewer.map(normalize_requested_reviewer), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReviewDismissedEvent( event, ) => PullRequestTimelineItem::ReviewDismissed { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::MergedEvent( event, ) => PullRequestTimelineItem::Merged { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ClosedEvent( event, ) => PullRequestTimelineItem::Closed { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReopenedEvent( event, ) => PullRequestTimelineItem::Reopened { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ConvertToDraftEvent( event, ) => PullRequestTimelineItem::ConvertToDraft { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReadyForReviewEvent( event, ) => PullRequestTimelineItem::ReadyForReview { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::HeadRefForcePushedEvent( event, ) => PullRequestTimelineItem::HeadRefForcePushed { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), - before_commit_oid: event.before_commit.map(|commit| commit.abbreviated_oid), - after_commit_oid: event.after_commit.map(|commit| commit.abbreviated_oid), + before_commit_oid: event + .before_commit + .map(|commit| commit.abbreviated_oid.into()), + after_commit_oid: event.after_commit.map(|commit| commit.abbreviated_oid.into()), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::BaseRefChangedEvent( event, ) => PullRequestTimelineItem::BaseRefChanged { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::LabeledEvent( event, ) => PullRequestTimelineItem::Labeled { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), - label: event.label.name, + label: event.label.name.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::UnlabeledEvent( event, ) => PullRequestTimelineItem::Unlabeled { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), - label: event.label.name, + label: event.label.name.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::MilestonedEvent( event, ) => PullRequestTimelineItem::Milestoned { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), - milestone_title: event.milestone_title, + milestone_title: event.milestone_title.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::DemilestonedEvent( event, ) => PullRequestTimelineItem::Demilestoned { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), - milestone_title: event.milestone_title, + milestone_title: event.milestone_title.into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReferencedEvent( event, ) => PullRequestTimelineItem::Referenced { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::CrossReferencedEvent( event, ) => PullRequestTimelineItem::CrossReferenced { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AutoMergeEnabledEvent( event, ) => PullRequestTimelineItem::AutoMergeEnabled { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AutoMergeDisabledEvent( event, ) => PullRequestTimelineItem::AutoMergeDisabled { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), - reason: event.reason.unwrap_or_default(), + reason: event.reason.unwrap_or_default().into(), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AddedToMergeQueueEvent( event, ) => PullRequestTimelineItem::AddedToMergeQueue { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::RemovedFromMergeQueueEvent( event, ) => PullRequestTimelineItem::RemovedFromMergeQueue { - created_at: event.created_at, + created_at: event.created_at.into(), actor: event.actor.map(normalize_actor), }, _ => PullRequestTimelineItem::Other { diff --git a/src/screen/dashboard/mod.rs b/src/screen/dashboard/mod.rs index fbb0c01..9598793 100644 --- a/src/screen/dashboard/mod.rs +++ b/src/screen/dashboard/mod.rs @@ -1,7 +1,9 @@ mod issue_list; +mod pull_request_body_view; mod pull_request_change_view; pub(crate) mod pull_request_diff_view; mod pull_request_file_tree; +pub(crate) mod pull_request_timeline_view; mod pull_request_view; mod screen; mod sidebar; diff --git a/src/screen/dashboard/pull_request_body_view.rs b/src/screen/dashboard/pull_request_body_view.rs new file mode 100644 index 0000000..43f51ef --- /dev/null +++ b/src/screen/dashboard/pull_request_body_view.rs @@ -0,0 +1,244 @@ +use std::sync::Arc; + +use gpui::{ + AppContext, InteractiveElement, ParentElement, StatefulInteractiveElement, Styled, div, img, + prelude::FluentBuilder, +}; + +use crate::{ + api, app, + component::{ + font_icon::{FontIcon, font_icon}, + markdown::{self, MarkdownText}, + text::text, + }, + query::{self, QueryStatus, read_query, use_query, watch_query}, +}; + +pub(crate) struct PullRequestBodyView { + pr_query: query::Entity, + markdown_viewer: Option>, +} + +impl PullRequestBodyView { + pub(crate) fn new(id: api::issues::Id, cx: &mut gpui::Context) -> Self { + let mut v = Self { + pr_query: use_query(api::issues::FetchPullRequest { id: id.clone() }, cx), + markdown_viewer: None, + }; + v.on_create(cx); + v + } + + fn on_create(&mut self, cx: &mut gpui::Context) { + _ = watch_query(&self.pr_query, Self::load_markdown_content, cx).detach(); + } + + fn load_markdown_content( + &mut self, + _query: &query::Entity, + cx: &mut gpui::Context, + ) { + let maybe_content = { + let data = read_query(&self.pr_query, cx); + if let QueryStatus::Loaded(pr) = data { + Some(Arc::clone(&pr.body)) + } else { + None + } + }; + + self.markdown_viewer = maybe_content.map(|content| cx.new(|cx| markdown::new(content, cx))); + + cx.notify(); + } + + fn pr_description( + &self, + pr: &api::issues::DetailedPullRequest, + cx: &gpui::Context, + ) -> gpui::Div { + let theme = app::current_theme(cx); + + let mut status_pill = div() + .flex() + .flex_row() + .items_center() + .gap_1() + .px_2() + .rounded_full(); + + match pr.state { + | api::issues::PullRequestState::Open => { + status_pill = status_pill + .bg(theme.colors.success_solid) + .child( + font_icon(FontIcon::PullRequestArrow) + .size_3() + .text_color(theme.colors.success_on_solid), + ) + .child( + text("Open") + .text_color(theme.colors.success_on_solid) + .text_xs(), + ); + } + | api::issues::PullRequestState::Closed => { + status_pill = status_pill + .bg(theme.colors.danger_solid) + .child( + font_icon(FontIcon::PullRequestClosed) + .size_3() + .text_color(theme.colors.danger_on_solid), + ) + .child( + text("Closed") + .text_color(theme.colors.danger_on_solid) + .text_xs(), + ); + } + | api::issues::PullRequestState::Merged => { + status_pill = status_pill.bg(theme.colors.accent_solid).child( + text("Merged") + .text_color(theme.colors.accent_on_solid) + .text_xs(), + ); + } + } + + let merge_text = pr.author.as_ref().map(|author| { + let base_branch = &pr.base_branch_name; + let head_branch = &pr.head_branch_name; + let str = format!( + "{} requested to merge {} into {}", + author.login, head_branch, base_branch + ); + + let head_branch_text_offset = author.login.len() + 20; + let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6; + + let highlights = [ + ( + 0..author.login.len(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), + ( + head_branch_text_offset..head_branch_text_offset + head_branch.len(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + color: Some(theme.colors.accent_fg.into()), + ..Default::default() + }, + ), + ( + base_branch_text_offset..base_branch_text_offset + base_branch.len(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + color: Some(theme.colors.accent_fg.into()), + ..Default::default() + }, + ), + ]; + + ( + author, + gpui::StyledText::new(str).with_highlights(highlights), + ) + }); + + let pr_title = gpui::SharedString::new(Arc::clone(&pr.title)); + + let metadata_line = + div() + .flex() + .flex_row() + .gap_2() + .when_some(merge_text, |it, (author, t)| { + it.child( + div() + .flex() + .flex_row() + .items_center() + .gap_1p5() + .child(img(author.avatar_url.as_ref()).size_4().rounded_full()) + .child( + div() + .min_w_0() + .w_full() + .text_color(theme.colors.text) + .text_xs() + .font_weight(gpui::FontWeight::LIGHT) + .opacity(0.8) + .child(t), + ), + ) + }); + + div() + .size_full() + .flex() + .flex_col() + .bg(theme.colors.surface) + .overflow_hidden() + .child( + div() + .flex() + .flex_row() + .justify_between() + .items_center() + .w_full() + .px_3p5() + .py_3() + .border_b_1() + .border_color(theme.colors.border_muted) + .child( + div() + .w_full() + .flex() + .flex_col() + .items_start() + .child(text(pr_title).w_full().text_xl().mb_1()) + .child(metadata_line), + ) + .child(div().flex().flex_col().items_end().gap_1().when_some( + pr.created_at, + |it, created_at| { + it.child( + text(created_at.format("%Y-%m-%d %H:%M").to_string()) + .opacity(0.5) + .text_xs(), + ) + .child(status_pill) + }, + )), + ) + .child( + div().flex_1().min_h_0().w_full().child( + div() + .id("pr-body-content") + .size_full() + .overflow_y_scroll() + .when_some(self.markdown_viewer.as_ref(), |it, viewer| { + it.child(div().w_full().p_3p5().child(viewer.clone())) + }), + ), + ) + } +} + +impl gpui::Render for PullRequestBodyView { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::prelude::Context, + ) -> impl gpui::IntoElement { + match read_query(&self.pr_query, cx) { + | QueryStatus::Err(e) => div().size_full().child(format!("{:?}", e)), + | QueryStatus::Loading => div().size_full().child("loading pr content"), + | QueryStatus::Loaded(pr) => self.pr_description(pr, cx), + } + } +} diff --git a/src/screen/dashboard/pull_request_timeline_view.rs b/src/screen/dashboard/pull_request_timeline_view.rs new file mode 100644 index 0000000..3281c1d --- /dev/null +++ b/src/screen/dashboard/pull_request_timeline_view.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use gpui::{ParentElement, Styled, div, px}; + +use crate::{ + api, app, + component::text::text, + query::{self, QueryStatus, read_query, use_query}, +}; + +pub(crate) struct PullRequestTimelineView { + timeline_list_state: gpui::ListState, + pr_timeline_query: query::Entity, +} + +impl PullRequestTimelineView { + pub(crate) fn new(id: api::issues::Id, cx: &mut gpui::Context) -> Self { + Self { + timeline_list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)), + pr_timeline_query: use_query( + api::issues::FetchPullRequestTimeline { + id, + first: 10, + after: None, + }, + cx, + ), + } + } +} + +impl gpui::Render for PullRequestTimelineView { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::prelude::Context, + ) -> impl gpui::IntoElement { + match read_query(&self.pr_timeline_query, cx) { + | QueryStatus::Loading => div().child("loading"), + | QueryStatus::Err(_) => div().child("error"), + + | QueryStatus::Loaded(data) => { + let items = data.items.iter().map(|item| match item { + | api::issues::PullRequestTimelineItem::Comment { body, author, .. } => { + comment_card(author.as_ref(), body.clone(), cx) + } + | _ => div(), + }); + + div().flex().flex_col().p_2().gap_2().children(items) + } + } + } +} + +fn comment_card( + author: Option<&api::issues::TimelineActor>, + body: Arc, + cx: &gpui::App, +) -> gpui::Div { + let theme = app::current_theme(cx); + div() + .flex() + .flex_col() + .border_1() + .border_color(theme.colors.border_muted) + .rounded_md() + .child( + div() + .flex() + .flex_row() + .items_center() + .px_2() + .pt_1p5() + .child( + div().text_xs().child( + match author.map(|it| it.name.clone()) { + | Some(author_name) => text(gpui::SharedString::from(author_name)), + | None => text("Unknown author"), + } + .text_color(theme.colors.text_subtle) + .font_weight(gpui::FontWeight::SEMIBOLD), + ), + ), + ) + .child(text(gpui::SharedString::from(body)).pl_7().text_sm().p_2()) +} diff --git a/src/screen/dashboard/pull_request_view.rs b/src/screen/dashboard/pull_request_view.rs index 84d972e..e33b6a3 100644 --- a/src/screen/dashboard/pull_request_view.rs +++ b/src/screen/dashboard/pull_request_view.rs @@ -1,31 +1,30 @@ -use std::sync::Arc; - -use gpui::{ - AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, - div, img, prelude::FluentBuilder, -}; +use gpui::{AppContext, IntoElement, ParentElement, Styled, div}; use crate::{ api::{self}, app, component::{ button::{self, Button, button}, - font_icon::{FontIcon, font_icon}, - markdown::{self, MarkdownText}, + font_icon::FontIcon, segmented_control::segmented_control, - text::text, }, - query::{self, QueryStatus, read_query, use_query, watch_query}, - screen::dashboard::pull_request_change_view::{self, PullRequestChangeView}, + screen::dashboard::{ + pull_request_body_view::PullRequestBodyView, + pull_request_change_view::{self, PullRequestChangeView}, + pull_request_timeline_view::PullRequestTimelineView, + }, }; pub(crate) struct PullRequestView { current_tab: Tab, - markdown_viewer: Option>, + pr_content: Option, diff_view: Option>, +} - pull_request_query: Option>, +struct PullRequestContent { + body: gpui::Entity, + timeline: gpui::Entity, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -37,9 +36,8 @@ enum Tab { pub fn new(_cx: &mut gpui::Context) -> PullRequestView { PullRequestView { current_tab: Tab::PullRequestBody, - markdown_viewer: None, + pr_content: None, diff_view: None, - pull_request_query: None, } } @@ -49,60 +47,12 @@ impl PullRequestView { id: api::issues::Id, cx: &mut gpui::Context, ) { - let query = use_query(api::issues::FetchPullRequest { id }, cx); - - self.pull_request_query = Some(query.clone()); self.current_tab = Tab::PullRequestBody; - - _ = watch_query(&query, Self::sync_pull_request_query, cx).detach(); - - cx.notify(); - } - - fn sync_pull_request_query( - &mut self, - _query: &query::Entity, - cx: &mut gpui::Context, - ) { - self.load_markdown_content(cx); - self.load_pr_diff(cx); - } - - fn load_markdown_content(&mut self, cx: &mut gpui::Context) { - let Some(query) = &self.pull_request_query else { - return; - }; - - let maybe_content = { - let data = read_query(&query, cx); - if let QueryStatus::Loaded(pr) = data { - Some(Arc::clone(&pr.body)) - } else { - None - } - }; - - self.markdown_viewer = maybe_content.map(|content| cx.new(|cx| markdown::new(content, cx))); - - cx.notify(); - } - - fn load_pr_diff(&mut self, cx: &mut gpui::Context) { - let Some(query) = &self.pull_request_query else { - return; - }; - - let pr_id = { - let data = read_query(&query, cx); - if let QueryStatus::Loaded(pr) = data { - Some(pr.id.clone()) - } else { - None - } - }; - - self.diff_view = pr_id.map(|id| cx.new(|cx| pull_request_change_view::new(id, cx))); - + self.pr_content = Some(PullRequestContent { + body: cx.new(|cx| PullRequestBodyView::new(id.clone(), cx)), + timeline: cx.new(|cx| PullRequestTimelineView::new(id.clone(), cx)), + }); + self.diff_view = Some(cx.new(|cx| pull_request_change_view::new(id, cx))); cx.notify(); } @@ -143,182 +93,6 @@ impl PullRequestView { ) .into_any_element() } - - fn pr_content( - &self, - pr: &api::issues::DetailedPullRequest, - cx: &gpui::Context, - ) -> gpui::AnyElement { - let theme = app::current_theme(cx); - - let mut status_pill = div() - .flex() - .flex_row() - .items_center() - .gap_1() - .px_2() - .rounded_full(); - - match pr.state { - | api::issues::PullRequestState::Open => { - status_pill = status_pill - .bg(theme.colors.success_solid) - .child( - font_icon(FontIcon::PullRequestArrow) - .size_3() - .text_color(theme.colors.success_on_solid), - ) - .child( - text("Open") - .text_color(theme.colors.success_on_solid) - .text_xs(), - ); - } - | api::issues::PullRequestState::Closed => { - status_pill = status_pill - .bg(theme.colors.danger_solid) - .child( - font_icon(FontIcon::PullRequestClosed) - .size_3() - .text_color(theme.colors.danger_on_solid), - ) - .child( - text("Closed") - .text_color(theme.colors.danger_on_solid) - .text_xs(), - ); - } - | api::issues::PullRequestState::Merged => { - status_pill = status_pill.bg(theme.colors.accent_solid).child( - text("Merged") - .text_color(theme.colors.accent_on_solid) - .text_xs(), - ); - } - } - - let merge_text = pr.author.as_ref().map(|author| { - let base_branch = &pr.base_branch_name; - let head_branch = &pr.head_branch_name; - let str = format!( - "{} requested to merge {} into {}", - author.login, head_branch, base_branch - ); - - let head_branch_text_offset = author.login.len() + 20; - let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6; - - let highlights = [ - ( - 0..author.login.len(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - head_branch_text_offset..head_branch_text_offset + head_branch.len(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - color: Some(theme.colors.accent_fg.into()), - ..Default::default() - }, - ), - ( - base_branch_text_offset..base_branch_text_offset + base_branch.len(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - color: Some(theme.colors.accent_fg.into()), - ..Default::default() - }, - ), - ]; - - ( - author, - gpui::StyledText::new(str).with_highlights(highlights), - ) - }); - - let pr_title = gpui::SharedString::new(Arc::clone(&pr.title)); - - let metadata_line = - div() - .flex() - .flex_row() - .gap_2() - .when_some(merge_text, |it, (author, t)| { - it.child( - div() - .flex() - .flex_row() - .items_center() - .gap_1p5() - .child(img(author.avatar_url.as_ref()).size_4().rounded_full()) - .child( - div() - .min_w_0() - .w_full() - .text_color(theme.colors.text) - .text_xs() - .font_weight(gpui::FontWeight::LIGHT) - .opacity(0.8) - .child(t), - ), - ) - }); - - div() - .size_full() - .flex() - .flex_col() - .bg(theme.colors.surface) - .overflow_hidden() - .child( - div() - .flex() - .flex_row() - .justify_between() - .items_center() - .w_full() - .px_3p5() - .py_3() - .border_b_1() - .border_color(theme.colors.border_muted) - .child( - div() - .w_full() - .flex() - .flex_col() - .items_start() - .child(text(pr_title).w_full().text_xl().mb_1()) - .child(metadata_line), - ) - .child(div().flex().flex_col().items_end().gap_1().when_some( - pr.created_at, - |it, created_at| { - it.child( - text(created_at.format("%Y-%m-%d %H:%M").to_string()) - .opacity(0.5) - .text_xs(), - ) - .child(status_pill) - }, - )), - ) - .child( - div().flex_1().min_h_0().w_full().child( - div() - .id("pr-body-content") - .size_full() - .overflow_y_scroll() - .when_some(self.markdown_viewer.as_ref(), |it, viewer| { - it.child(div().w_full().p_3p5().child(viewer.clone())) - }), - ), - ) - .into_any_element() - } } impl gpui::Render for PullRequestView { @@ -327,31 +101,43 @@ impl gpui::Render for PullRequestView { _window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { + let theme = app::current_theme(cx); div() .size_full() .flex() .flex_col() .child(self.toolbar(cx)) - .child(match &self.pull_request_query { - | Some(q) => { - match read_query(q, cx) { - | QueryStatus::Loaded(pr) => match (&self.diff_view, self.current_tab) { - | (Some(diff_view), Tab::DiffView) => diff_view.clone().into_any_element(), - | _ => self.pr_content(pr, cx), + .child( + match (&self.diff_view, &self.pr_content, self.current_tab) { + | (Some(diff_view), _, Tab::DiffView) => diff_view.clone().into_any_element(), + + | (_, Some(pr_content), Tab::PullRequestBody) => div() + .size_full() + .flex() + .flex_row() + .child( + div() + .flex_1() + .flex() + .flex_col() + .border_r_1() + .border_color(theme.colors.border_muted) + .child(pr_content.body.clone()), + ) + .child( + div() + .flex_1() + .bg(theme.colors.surface) + .child(pr_content.timeline.clone()), + ) + .into_any_element(), + + | (_, None, Tab::PullRequestBody) => { + div().size_full().child("no pr selected").into_any_element() + } + + | _ => panic!("should not happen"), }, - - | QueryStatus::Err(e) => div() - .size_full() - .child(format!("{:?}", e)) - .into_any_element(), - | QueryStatus::Loading => div() - .size_full() - .child("loading pr content") - .into_any_element(), - } - } - - | None => div().size_full().child("no pr selected").into_any_element(), - }) + ) } } diff --git a/src/theme/catppuccin.rs b/src/theme/catppuccin.rs index 87c49dc..689ed5f 100644 --- a/src/theme/catppuccin.rs +++ b/src/theme/catppuccin.rs @@ -158,7 +158,7 @@ pub(crate) fn mocha() -> Theme { colors: ThemeColors { background: hex(0x1e1e2e), surface: hex(0x181825), - surface_elevated: hex(0x45475a), + surface_elevated: hex(0x363a4f), surface_button: linear_gradient( 180., linear_color_stop(hex(0x4f5068), 0.),