Skip to content

Commit 72b5c9c

Browse files
authored
Merge pull request #1 from github/jg/pr-ops
Better CLI + bundle of MCP
2 parents d5f6734 + 6806dc5 commit 72b5c9c

File tree

9 files changed

+668
-473
lines changed

9 files changed

+668
-473
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
dist/
33
*.tsbuildinfo
4+
bin/gofumpt

cli/cmd/engine-cli/display.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
3+
package main
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/charmbracelet/lipgloss"
11+
"github.com/github/copilot-engine-sdk/cli/internal/events"
12+
"github.com/github/copilot-engine-sdk/cli/internal/server"
13+
)
14+
15+
var (
16+
dimStyle = lipgloss.NewStyle().Faint(true)
17+
boldStyle = lipgloss.NewStyle().Bold(true)
18+
greenStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
19+
redStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
20+
yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3"))
21+
cyanStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6"))
22+
magStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("5"))
23+
mutedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
24+
25+
separator = dimStyle.Render(" ─────────────────────────────────────────")
26+
)
27+
28+
func resolveKind(envelopeKind string, content json.RawMessage) string {
29+
if envelopeKind != "" && envelopeKind != "log" {
30+
return envelopeKind
31+
}
32+
var m struct {
33+
Kind string `json:"kind"`
34+
}
35+
if json.Unmarshal(content, &m) == nil && m.Kind != "" {
36+
return m.Kind
37+
}
38+
return "unknown"
39+
}
40+
41+
// isDisplayEvent returns true for events we render.
42+
func isDisplayEvent(kind string) bool {
43+
switch kind {
44+
case "message", "tool_execution", "report_progress", "pr_summary",
45+
"model_call_failure", "comment_reply":
46+
return true
47+
default:
48+
return false
49+
}
50+
}
51+
52+
// printEvent renders a single event. Returns true if something was printed.
53+
func printEvent(event server.ProgressEvent) bool {
54+
kind := resolveKind(event.Kind, event.Content)
55+
56+
switch kind {
57+
case "message":
58+
return printMessage(event.Content)
59+
case "tool_execution":
60+
return printToolExecution(event.Content)
61+
case "report_progress":
62+
return printReportProgress(event.Content)
63+
case "pr_summary":
64+
return printPRSummary(event.Content)
65+
case "model_call_failure":
66+
return printModelCallFailure(event.Content)
67+
case "comment_reply":
68+
return printCommentReply(event.Content)
69+
default:
70+
return false
71+
}
72+
}
73+
74+
// ─────────────────────────────────────────────────────────────
75+
// Assistant messages
76+
// ─────────────────────────────────────────────────────────────
77+
78+
func printMessage(raw json.RawMessage) bool {
79+
var ev events.Message
80+
if json.Unmarshal(raw, &ev) != nil {
81+
return false
82+
}
83+
content := strings.TrimSpace(ev.Message.Content)
84+
if content == "" {
85+
return false
86+
}
87+
88+
switch ev.Message.Role {
89+
case "assistant":
90+
// Skip raw XML wrapper messages (pr_title/pr_description)
91+
if strings.HasPrefix(content, "<pr_title>") {
92+
return false
93+
}
94+
fmt.Println(separator)
95+
fmt.Printf(" %s %s\n", cyanStyle.Render("●"), boldStyle.Render("Assistant"))
96+
fmt.Println()
97+
for _, line := range wrapText(content, 74) {
98+
fmt.Printf(" %s\n", line)
99+
}
100+
return true
101+
102+
case "tool":
103+
name := ev.ToolName
104+
if name == "" {
105+
name = "tool"
106+
}
107+
// Skip report_progress tool results — shown via ↑ Progress block
108+
if strings.Contains(name, "report_progress") {
109+
return false
110+
}
111+
fmt.Println(separator)
112+
isErr := containsError(content)
113+
if isErr {
114+
fmt.Printf(" %s %s %s\n", redStyle.Render("●"), dimStyle.Render("Tool:"), yellowStyle.Render(name))
115+
} else {
116+
fmt.Printf(" %s %s %s\n", greenStyle.Render("●"), dimStyle.Render("Tool:"), yellowStyle.Render(name))
117+
}
118+
// Show the tool output, indented and muted
119+
fmt.Println()
120+
lines := wrapText(content, 70)
121+
maxLines := 8
122+
for i, line := range lines {
123+
if i >= maxLines {
124+
fmt.Printf(" %s\n", mutedStyle.Render(fmt.Sprintf("... (%d more lines)", len(lines)-i)))
125+
break
126+
}
127+
fmt.Printf(" %s\n", mutedStyle.Render(line))
128+
}
129+
return true
130+
131+
default:
132+
return false
133+
}
134+
}
135+
136+
// ─────────────────────────────────────────────────────────────
137+
// Tool execution results
138+
// ─────────────────────────────────────────────────────────────
139+
140+
func printToolExecution(raw json.RawMessage) bool {
141+
var ev events.ToolExecution
142+
if json.Unmarshal(raw, &ev) != nil {
143+
return false
144+
}
145+
name := ev.ToolName
146+
if name == "" {
147+
name = truncate(ev.ToolCallID, 20)
148+
}
149+
// Skip report_progress — already shown via ↑ Progress block
150+
if strings.Contains(name, "report_progress") {
151+
return false
152+
}
153+
// tool_execution is redundant with the tool message above — skip it
154+
// The tool message (role=tool) already shows name + ✓/✗ + output
155+
return false
156+
}
157+
158+
// ─────────────────────────────────────────────────────────────
159+
// Progress updates
160+
// ─────────────────────────────────────────────────────────────
161+
162+
func printReportProgress(raw json.RawMessage) bool {
163+
var ev events.ReportProgress
164+
if json.Unmarshal(raw, &ev) != nil {
165+
return false
166+
}
167+
fmt.Println(separator)
168+
title := ev.PRTitle
169+
if title == "" {
170+
title = "Progress"
171+
}
172+
fmt.Printf(" %s %s\n", greenStyle.Render("●"), boldStyle.Render(title))
173+
if ev.PRDescription != "" {
174+
fmt.Println()
175+
for _, line := range strings.Split(ev.PRDescription, "\n") {
176+
trimmed := strings.TrimSpace(line)
177+
if trimmed == "" {
178+
continue
179+
}
180+
switch {
181+
case strings.HasPrefix(trimmed, "- [x]"):
182+
fmt.Printf(" %s\n", greenStyle.Render(trimmed))
183+
case strings.HasPrefix(trimmed, "- [ ]"):
184+
fmt.Printf(" %s\n", yellowStyle.Render(trimmed))
185+
default:
186+
fmt.Printf(" %s\n", dimStyle.Render(trimmed))
187+
}
188+
}
189+
}
190+
return true
191+
}
192+
193+
// ─────────────────────────────────────────────────────────────
194+
// PR Summary
195+
// ─────────────────────────────────────────────────────────────
196+
197+
func printPRSummary(raw json.RawMessage) bool {
198+
var ev events.PRSummary
199+
if json.Unmarshal(raw, &ev) != nil {
200+
return false
201+
}
202+
fmt.Println(separator)
203+
fmt.Printf(" %s %s %s\n", magStyle.Render("●"), dimStyle.Render("PR"), boldStyle.Render(ev.PRTitle))
204+
if ev.PRDescription != "" {
205+
fmt.Println()
206+
lines := strings.Split(ev.PRDescription, "\n")
207+
for i, line := range lines {
208+
if i >= 15 {
209+
fmt.Printf(" %s\n", mutedStyle.Render(fmt.Sprintf("... (%d more lines)", len(lines)-i)))
210+
break
211+
}
212+
fmt.Printf(" %s\n", line)
213+
}
214+
}
215+
return true
216+
}
217+
218+
// ─────────────────────────────────────────────────────────────
219+
// Errors and misc
220+
// ─────────────────────────────────────────────────────────────
221+
222+
func printModelCallFailure(raw json.RawMessage) bool {
223+
var ev events.ModelCallFailure
224+
if json.Unmarshal(raw, &ev) != nil {
225+
return false
226+
}
227+
if ev.ModelCall.Error != "" {
228+
fmt.Println(separator)
229+
fmt.Printf(" %s %s\n", redStyle.Render("●"), redStyle.Render(truncate(ev.ModelCall.Error, 72)))
230+
return true
231+
}
232+
return false
233+
}
234+
235+
func printCommentReply(raw json.RawMessage) bool {
236+
var ev events.CommentReply
237+
if json.Unmarshal(raw, &ev) != nil {
238+
return false
239+
}
240+
fmt.Println(separator)
241+
fmt.Printf(" %s %s #%d\n", cyanStyle.Render("●"), dimStyle.Render("Reply to"), ev.CommentID)
242+
if ev.Message != "" {
243+
fmt.Println()
244+
for _, line := range wrapText(ev.Message, 74) {
245+
fmt.Printf(" %s\n", line)
246+
}
247+
}
248+
return true
249+
}
250+
251+
// ─────────────────────────────────────────────────────────────
252+
// Helpers
253+
// ─────────────────────────────────────────────────────────────
254+
255+
func containsError(s string) bool {
256+
lower := strings.ToLower(s)
257+
return strings.Contains(lower, "error") ||
258+
strings.Contains(lower, "denied") ||
259+
strings.Contains(lower, "failed") ||
260+
strings.Contains(lower, "not found")
261+
}
262+
263+
func wrapText(text string, width int) []string {
264+
var lines []string
265+
for _, paragraph := range strings.Split(text, "\n") {
266+
if len(paragraph) <= width {
267+
lines = append(lines, paragraph)
268+
continue
269+
}
270+
remaining := paragraph
271+
for len(remaining) > width {
272+
idx := width
273+
for idx > 0 && remaining[idx] != ' ' {
274+
idx--
275+
}
276+
if idx == 0 {
277+
idx = width
278+
}
279+
lines = append(lines, remaining[:idx])
280+
remaining = remaining[idx:]
281+
if len(remaining) > 0 && remaining[0] == ' ' {
282+
remaining = remaining[1:]
283+
}
284+
}
285+
if len(remaining) > 0 {
286+
lines = append(lines, remaining)
287+
}
288+
}
289+
return lines
290+
}
291+
292+
func truncate(s string, maxLen int) string {
293+
if len(s) <= maxLen {
294+
return s
295+
}
296+
return s[:maxLen-3] + "..."
297+
}

0 commit comments

Comments
 (0)