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
1 change: 1 addition & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type App struct {
GitGutterGen int
GitGutterTimer *time.Timer
Version string
CurrentPR *ActivePR
}

func (a *App) KeyFor(cmd string) string {
Expand Down
37 changes: 37 additions & 0 deletions internal/app/commands_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,41 @@ func registerPRCommands(app *App) {
Keywords: []string{"git", "pull request", "github"},
Handler: func() { app.Changes.RemovePRGroups() },
})

reg.Register(command.Command{
ID: "pr.comments", Title: "Git: PR Comments",
Keywords: []string{"git", "pull request", "github", "review", "comments"},
Handler: func() {
if app.Root.HasOverlay() {
return
}
if app.CurrentPR == nil {
app.StatusWarn("No active PR. Use 'Git: Review PR' first.")
return
}
app.FetchPRComments(
app.CurrentPR.Owner,
app.CurrentPR.Repo,
app.CurrentPR.Number,
app.CurrentPR.Title,
app.CurrentPR.HeadSHA,
)
},
})

reg.Register(command.Command{
ID: "pr.addComment", Title: "Git: Add PR Comment",
Keywords: []string{"git", "pull request", "github", "comment", "review"},
Handler: func() {
if app.CurrentPR == nil {
app.StatusWarn("No active PR. Use 'Git: Review PR' first.")
return
}
app.ShowInputDialog("Add PR Comment", "Type your comment...", "", func(body string) {
if body != "" {
app.AddPRGeneralComment(app.CurrentPR, body)
}
})
},
})
}
30 changes: 30 additions & 0 deletions internal/app/eventloop.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,41 @@ func RunEventLoop(
dv.FinishLoading()
}
}
case *PrCommentsFetchResult:
if v.Err != nil {
app.StatusError("Failed to fetch PR comments: " + v.Err.Error())
} else {
app.CurrentPR = &ActivePR{
Owner: v.Owner,
Repo: v.Repo,
Number: v.Number,
Title: v.Title,
HeadSHA: v.HeadSHA,
}
app.ShowReviewHub(v.Comments, v.Title, v.Number)
}
case *PrAddCommentResult:
if v.Err != nil {
app.StatusError("Failed to post comment: " + v.Err.Error())
} else {
app.StatusNotify("Comment posted successfully")
// Refetch comments to show the new one
if app.CurrentPR != nil {
app.FetchPRComments(v.Owner, v.Repo, v.Number, v.Title, app.CurrentPR.HeadSHA)
}
}
case *PrFetchResult:
app.Changes.Loading = false
if v.Err != nil {
app.StatusError("PR fetch failed: " + v.Err.Error())
} else {
app.CurrentPR = &ActivePR{
Owner: v.Info.Owner,
Repo: v.Info.Repo,
Number: v.Info.Number,
Title: v.Info.Title,
HeadSHA: v.Info.HeadSHA,
}
var files []git.FileStatus
for _, f := range v.Info.Files {
files = append(files, git.FileStatus{
Expand Down
138 changes: 138 additions & 0 deletions internal/app/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/eugenioenko/ttt/internal/github"
"github.com/eugenioenko/ttt/internal/ui"

"github.com/gdamore/tcell/v2"
)
Expand All @@ -15,13 +16,150 @@ type PrFetchResult struct {
Err error
}

// PrCommentsFetchResult carries PR comments fetched asynchronously.
type PrCommentsFetchResult struct {
Owner string
Repo string
Number int
Title string
HeadSHA string
Comments []github.PRComment
Err error
}

// PrAddCommentResult carries the result of adding a comment.
type PrAddCommentResult struct {
Err error
// Refetch triggers
Owner string
Repo string
Number int
Title string
}

type DiffContentResult struct {
TabName string
OldLines []string
NewLines []string
Err error
}

// ActivePR tracks the currently loaded PR for comment operations.
type ActivePR struct {
Owner string
Repo string
Number int
Title string
HeadSHA string
}

// FetchPRComments fetches comments for a PR asynchronously.
func (a *App) FetchPRComments(owner, repo string, number int, title, headSHA string) {
a.StatusNotify(fmt.Sprintf("Fetching PR #%d comments...", number))
go func() {
comments, err := github.FetchPRComments(owner, repo, number)
a.Screen.PostEvent(tcell.NewEventInterrupt(&PrCommentsFetchResult{
Owner: owner,
Repo: repo,
Number: number,
Title: title,
HeadSHA: headSHA,
Comments: comments,
Err: err,
}))
}()
}

// AddPRComment adds a general comment to the active PR.
func (a *App) AddPRGeneralComment(pr *ActivePR, body string) {
a.StatusNotify("Posting comment...")
go func() {
err := github.AddPRComment(pr.Owner, pr.Repo, pr.Number, body)
a.Screen.PostEvent(tcell.NewEventInterrupt(&PrAddCommentResult{
Err: err,
Owner: pr.Owner,
Repo: pr.Repo,
Number: pr.Number,
Title: pr.Title,
}))
}()
}

// AddPRInlineComment adds an inline comment on a specific file/line.
func (a *App) AddPRInlineComment(pr *ActivePR, body, path string, line int) {
a.StatusNotify("Posting inline comment...")
go func() {
err := github.AddPRInlineComment(pr.Owner, pr.Repo, pr.Number, body, path, line, pr.HeadSHA)
a.Screen.PostEvent(tcell.NewEventInterrupt(&PrAddCommentResult{
Err: err,
Owner: pr.Owner,
Repo: pr.Repo,
Number: pr.Number,
Title: pr.Title,
}))
}()
}

// ShowReviewHub displays the review hub overlay with all PR comments.
func (a *App) ShowReviewHub(comments []github.PRComment, title string, number int) {
// Convert github.PRComment to ui.ReviewComment
var uiComments []ui.ReviewComment
for _, c := range comments {
uiComments = append(uiComments, ui.ReviewComment{
ID: c.ID,
Body: c.Body,
User: c.User,
CreatedAt: c.CreatedAt,
Path: c.Path,
Line: c.Line,
IsInline: c.IsInline,
})
}

hub := ui.NewReviewHubWidget(uiComments, title, number)
hub.Borders = a.Borders
hub.OnDismiss = func() {
a.DismissDialog()
}
hub.OnNavigate = func(path string, line int) {
a.DismissDialog()
// Try to find file in the workspace and open it
if path != "" {
a.EditorGroup.OpenFile(path)
if line > 0 {
a.EditorGroup.GoToLine(line)
}
a.Root.SetFocus(a.EditorGroup)
}
}
hub.OnAddComment = func() {
if a.CurrentPR == nil {
a.StatusWarn("No active PR")
return
}
a.DismissDialog()
a.ShowInputDialog("Add PR Comment", "Type your comment...", "", func(body string) {
if body != "" {
a.AddPRGeneralComment(a.CurrentPR, body)
}
})
}
hub.OnAddInlineComment = func(path string, line int) {
if a.CurrentPR == nil {
a.StatusWarn("No active PR")
return
}
a.DismissDialog()
placeholder := fmt.Sprintf("Reply to %s:%d", path, line)
a.ShowInputDialog("Add Inline Comment", placeholder, "", func(body string) {
if body != "" {
a.AddPRInlineComment(a.CurrentPR, body, path, line)
}
})
}
a.ShowDialog(hub)
}

func (a *App) FetchAndOpenPR(url string) {
owner, repo, number, err := github.ParsePRURL(url)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions internal/app/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ func BuildStyleMap(theme config.ThemeConfig) term.StyleMap {
applyStyleDef(&m, term.StyleSuccess, theme.Success)
applyStyleDef(&m, term.StyleDanger, theme.Danger)
applyStyleDef(&m, term.StyleWarning, theme.Warning)
applyStyleDef(&m, term.StyleCommentMarker, theme.Comment.Marker)
applyStyleDef(&m, term.StyleCommentUser, theme.Comment.User)
applyStyleDef(&m, term.StyleCommentBody, theme.Comment.Body)
applyStyleDef(&m, term.StyleCommentFile, theme.Comment.File)

applyDiagStyle(&m, term.StyleDiagError, theme.Editor.Diagnostics.Error)
applyDiagStyle(&m, term.StyleDiagWarning, theme.Editor.Diagnostics.Warning)
Expand Down
1 change: 1 addition & 0 deletions internal/config/keybindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ func DefaultKeybindings() []KeyBinding {
{Key: "ctrl+k b", Command: "panel.toggle"},
{Key: "ctrl+k j", Command: "editor.joinLines"},
{Key: "ctrl+k y", Command: "view.keybindings"},
{Key: "ctrl+k g", Command: "pr.comments"},
{Key: "ctrl+t", Command: "terminal.toggle"},
{Key: "alt+t", Command: "terminal.fullscreen"},
{Key: "f10", Command: "menu.file"},
Expand Down
22 changes: 17 additions & 5 deletions internal/config/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ type HoverStyles struct {
Code StyleDef `json:"code"`
}

type CommentStyles struct {
Marker StyleDef `json:"marker"`
User StyleDef `json:"user"`
Body StyleDef `json:"body"`
File StyleDef `json:"file"`
}

type ThemeConfig struct {
Default StyleDef `json:"default"`
Muted StyleDef `json:"muted"`
Expand All @@ -200,6 +207,7 @@ type ThemeConfig struct {
Input InputStyles `json:"input"`
Hover HoverStyles `json:"hover"`
Border StyleDef `json:"border"`
Comment CommentStyles `json:"comment,omitempty"`
Diff DiffStyles `json:"diff"`
Scrollbar StyleDef `json:"scrollbar"`
Syntax SyntaxStyles `json:"syntax"`
Expand Down Expand Up @@ -235,11 +243,11 @@ func DefaultTheme() ThemeConfig {
Border: StyleDef{Fg: "#555555"},

Editor: EditorStyles{
ActiveLine: StyleDef{Bg: "#282828"},
Selection: StyleDef{Bg: "#282828"},
LineNumber: StyleDef{Fg: "#999999"},
SearchMatch: StyleDef{Bg: "#623800"},
SearchActive: StyleDef{Bg: "#9e6a03"},
ActiveLine: StyleDef{Bg: "#282828"},
Selection: StyleDef{Bg: "#282828"},
LineNumber: StyleDef{Fg: "#999999"},
SearchMatch: StyleDef{Bg: "#623800"},
SearchActive: StyleDef{Bg: "#9e6a03"},
BracketMatch: StyleDef{Bg: "#3a3a3a"},
BracketColors: []string{"yellow", "magenta", "blue"},
},
Expand Down Expand Up @@ -300,6 +308,10 @@ func (t *ThemeConfig) ResolveColors() {
}
fillFg(&t.Hover.Bold, t.Default.Fg)
fillFg(&t.Hover.Code, t.Syntax.String.Fg)
fillFg(&t.Comment.Marker, "#569cd6")
fillFg(&t.Comment.User, "#dcdcaa")
fillFg(&t.Comment.Body, t.Default.Fg)
fillFg(&t.Comment.File, "#569cd6")
}

func fillFg(s *StyleDef, color string) {
Expand Down
Loading
Loading