Skip to content

Commit 27ec5c8

Browse files
authored
Working PR ops
1 parent d5f6734 commit 27ec5c8

File tree

6 files changed

+653
-405
lines changed

6 files changed

+653
-405
lines changed

cli/cmd/engine-cli/display.go

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
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/github/copilot-engine-sdk/cli/internal/events"
11+
"github.com/github/copilot-engine-sdk/cli/internal/server"
12+
)
13+
14+
// ANSI color codes
15+
const (
16+
colorReset = "\033[0m"
17+
colorBold = "\033[1m"
18+
colorDim = "\033[2m"
19+
colorCyan = "\033[36m"
20+
colorGreen = "\033[32m"
21+
colorYellow = "\033[33m"
22+
colorBlue = "\033[34m"
23+
colorMagenta = "\033[35m"
24+
colorRed = "\033[31m"
25+
colorWhite = "\033[37m"
26+
colorGray = "\033[90m"
27+
)
28+
29+
func eventIcon(kind string) string {
30+
switch kind {
31+
case "message":
32+
return "💬"
33+
case "model_call_success":
34+
return "✨"
35+
case "model_call_failure":
36+
return "❌"
37+
case "tool_execution":
38+
return "🔧"
39+
case "response":
40+
return "📤"
41+
case "history_truncated":
42+
return "✂️"
43+
case "report_progress":
44+
return "📝"
45+
case "comment_reply":
46+
return "💬"
47+
case "pr_summary":
48+
return "📋"
49+
default:
50+
return "📨"
51+
}
52+
}
53+
54+
func eventColor(kind string) string {
55+
switch kind {
56+
case "message":
57+
return colorCyan
58+
case "model_call_success":
59+
return colorGreen
60+
case "model_call_failure":
61+
return colorRed
62+
case "tool_execution":
63+
return colorYellow
64+
case "response":
65+
return colorMagenta
66+
case "history_truncated":
67+
return colorBlue
68+
case "report_progress":
69+
return colorGreen
70+
case "comment_reply":
71+
return colorCyan
72+
case "pr_summary":
73+
return colorMagenta
74+
default:
75+
return colorWhite
76+
}
77+
}
78+
79+
// resolveKind extracts the semantic event kind. The progress envelope uses
80+
// kind="log" for all regular sessions-v2 events, so we look inside the
81+
// content JSON for the real kind in that case.
82+
func resolveKind(envelopeKind string, content json.RawMessage) string {
83+
if envelopeKind != "" && envelopeKind != "log" {
84+
return envelopeKind
85+
}
86+
return extractKind(content)
87+
}
88+
89+
func printProgressEvent(event server.ProgressEvent) {
90+
kind := resolveKind(event.Kind, event.Content)
91+
icon := eventIcon(kind)
92+
color := eventColor(kind)
93+
94+
// Print event header with box drawing
95+
fmt.Printf("┌─ %s %s%s%s%s\n", icon, color, colorBold, kind, colorReset)
96+
97+
// Print event details
98+
printEventDetails(kind, event.Content)
99+
100+
if verbose {
101+
// In verbose mode, show pretty-printed JSON
102+
var raw any
103+
if json.Unmarshal(event.Content, &raw) == nil {
104+
pretty, _ := json.MarshalIndent(raw, "│ ", " ")
105+
fmt.Printf("│ %s%s%s\n", colorGray, string(pretty), colorReset)
106+
}
107+
}
108+
109+
fmt.Println("└─")
110+
}
111+
112+
// hasEventDetails returns true if the event kind has meaningful content to render
113+
// in the box format. Events without details are rendered as compact inline counters.
114+
func hasEventDetails(kind string, raw json.RawMessage) bool {
115+
switch kind {
116+
case "message", "model_call_success", "model_call_failure", "tool_execution",
117+
"response", "history_truncated", "report_progress", "comment_reply", "pr_summary":
118+
return true
119+
default:
120+
return false
121+
}
122+
}
123+
124+
func printEventDetails(kind string, raw json.RawMessage) {
125+
switch kind {
126+
case "message":
127+
var ev events.Message
128+
if json.Unmarshal(raw, &ev) != nil {
129+
return
130+
}
131+
role := ev.Message.Role
132+
roleColor := colorCyan
133+
switch role {
134+
case "assistant":
135+
roleColor = colorGreen
136+
case "user":
137+
roleColor = colorYellow
138+
case "tool":
139+
roleColor = colorMagenta
140+
}
141+
142+
if role == "tool" && ev.ToolName != "" {
143+
fmt.Printf("│ %sRole:%s %s%s%s %sTool:%s %s%s%s\n",
144+
colorDim, colorReset, roleColor, role, colorReset,
145+
colorDim, colorReset, colorYellow, ev.ToolName, colorReset)
146+
} else {
147+
fmt.Printf("│ %sRole:%s %s%s%s\n", colorDim, colorReset, roleColor, role, colorReset)
148+
}
149+
150+
if ev.Message.Content != "" {
151+
printWrapped(ev.Message.Content, colorWhite, 3)
152+
}
153+
154+
case "model_call_success":
155+
var ev events.ModelCallSuccess
156+
if json.Unmarshal(raw, &ev) != nil {
157+
return
158+
}
159+
if ev.ModelCall.Model != "" {
160+
fmt.Printf("│ %sModel:%s %s\n", colorDim, colorReset, ev.ModelCall.Model)
161+
}
162+
if ev.ResponseUsage.PromptTokens > 0 || ev.ResponseUsage.CompletionTokens > 0 {
163+
fmt.Printf("│ %sTokens:%s %s%d%s prompt → %s%d%s completion\n",
164+
colorDim, colorReset,
165+
colorYellow, ev.ResponseUsage.PromptTokens, colorReset,
166+
colorGreen, ev.ResponseUsage.CompletionTokens, colorReset)
167+
}
168+
if len(ev.ResponseChunk.Choices) > 0 {
169+
if text := ev.ResponseChunk.Choices[0].Delta.Content; text != "" {
170+
printWrapped(text, colorGreen, 3)
171+
}
172+
}
173+
174+
case "model_call_failure":
175+
var ev events.ModelCallFailure
176+
if json.Unmarshal(raw, &ev) != nil {
177+
return
178+
}
179+
if ev.ModelCall.Error != "" {
180+
fmt.Printf("│ %sError:%s %s%s%s\n", colorDim, colorReset, colorRed, truncate(ev.ModelCall.Error, 80), colorReset)
181+
}
182+
183+
case "tool_execution":
184+
var ev events.ToolExecution
185+
if json.Unmarshal(raw, &ev) != nil {
186+
return
187+
}
188+
name := ev.ToolName
189+
if name == "" {
190+
name = truncate(ev.ToolCallID, 23)
191+
}
192+
status := ev.ToolResult.ResultType
193+
statusColor := colorYellow
194+
switch status {
195+
case "success":
196+
statusColor = colorGreen
197+
case "failure", "error":
198+
statusColor = colorRed
199+
}
200+
fmt.Printf("│ %sTool:%s %s%s%s %sStatus:%s %s%s%s\n",
201+
colorDim, colorReset, colorYellow, name, colorReset,
202+
colorDim, colorReset, statusColor, status, colorReset)
203+
204+
case "response":
205+
var ev events.Response
206+
if json.Unmarshal(raw, &ev) != nil {
207+
return
208+
}
209+
if ev.Response.Content != "" {
210+
printWrapped(ev.Response.Content, colorMagenta, 3)
211+
}
212+
213+
case "history_truncated":
214+
var ev events.HistoryTruncated
215+
if json.Unmarshal(raw, &ev) != nil {
216+
return
217+
}
218+
fmt.Printf("│ %sMessages:%s %s%d%s → %s%d%s\n",
219+
colorDim, colorReset,
220+
colorRed, ev.TruncateResult.PreTruncationMessagesLength, colorReset,
221+
colorGreen, ev.TruncateResult.PostTruncationMessagesLength, colorReset)
222+
223+
case "report_progress":
224+
var ev events.ReportProgress
225+
if json.Unmarshal(raw, &ev) != nil {
226+
return
227+
}
228+
if ev.PRTitle != "" {
229+
fmt.Printf("│ %sTitle:%s %s%s%s\n", colorDim, colorReset, colorBold, ev.PRTitle, colorReset)
230+
}
231+
if ev.PRDescription != "" {
232+
lines := strings.Split(ev.PRDescription, "\n")
233+
fmt.Printf("│ %sDescription:%s\n", colorDim, colorReset)
234+
for i, line := range lines {
235+
if i >= 10 {
236+
fmt.Printf("│ %s... (%d more lines)%s\n", colorDim, len(lines)-i, colorReset)
237+
break
238+
}
239+
trimmed := strings.TrimSpace(line)
240+
switch {
241+
case strings.HasPrefix(trimmed, "- [x]"):
242+
fmt.Printf("│ %s%s%s\n", colorGreen, line, colorReset)
243+
case strings.HasPrefix(trimmed, "- [ ]"):
244+
fmt.Printf("│ %s%s%s\n", colorYellow, line, colorReset)
245+
default:
246+
fmt.Printf("│ %s\n", line)
247+
}
248+
}
249+
}
250+
251+
case "comment_reply":
252+
var ev events.CommentReply
253+
if json.Unmarshal(raw, &ev) != nil {
254+
return
255+
}
256+
if ev.CommentID != 0 {
257+
fmt.Printf("│ %sComment ID:%s %d\n", colorDim, colorReset, ev.CommentID)
258+
}
259+
if ev.Message != "" {
260+
printWrapped(ev.Message, colorCyan, 5)
261+
}
262+
263+
case "pr_summary":
264+
var ev events.PRSummary
265+
if json.Unmarshal(raw, &ev) != nil {
266+
return
267+
}
268+
fmt.Printf("│ %s━━━ Final PR Summary ━━━%s\n", colorBold, colorReset)
269+
if ev.PRTitle != "" {
270+
fmt.Printf("│ %sTitle:%s %s%s%s\n", colorDim, colorReset, colorBold+colorMagenta, ev.PRTitle, colorReset)
271+
}
272+
if ev.PRDescription != "" {
273+
lines := strings.Split(ev.PRDescription, "\n")
274+
fmt.Printf("│ %sDescription:%s\n", colorDim, colorReset)
275+
for i, line := range lines {
276+
if i >= 15 {
277+
fmt.Printf("│ %s... (%d more lines)%s\n", colorDim, len(lines)-i, colorReset)
278+
break
279+
}
280+
fmt.Printf("│ %s\n", line)
281+
}
282+
}
283+
}
284+
}
285+
286+
func printWrapped(text, color string, maxLines int) {
287+
wrapped := wrapText(text, 70)
288+
for i, line := range wrapped {
289+
if i == 0 {
290+
fmt.Printf("│ %s\"%s\"%s\n", color, line, colorReset)
291+
} else {
292+
fmt.Printf("│ %s %s%s\n", color, line, colorReset)
293+
}
294+
if i >= maxLines-1 {
295+
fmt.Printf("│ %s...%s\n", colorDim, colorReset)
296+
break
297+
}
298+
}
299+
}
300+
301+
func wrapText(text string, width int) []string {
302+
var lines []string
303+
// Split on newlines first, then wrap each line by width
304+
for _, paragraph := range strings.Split(text, "\n") {
305+
if len(paragraph) <= width {
306+
lines = append(lines, paragraph)
307+
continue
308+
}
309+
remaining := paragraph
310+
for len(remaining) > width {
311+
idx := width
312+
for idx > 0 && remaining[idx] != ' ' {
313+
idx--
314+
}
315+
if idx == 0 {
316+
idx = width
317+
}
318+
lines = append(lines, remaining[:idx])
319+
remaining = remaining[idx:]
320+
if len(remaining) > 0 && remaining[0] == ' ' {
321+
remaining = remaining[1:]
322+
}
323+
}
324+
if len(remaining) > 0 {
325+
lines = append(lines, remaining)
326+
}
327+
}
328+
return lines
329+
}
330+
331+
func extractKind(raw json.RawMessage) string {
332+
var m struct {
333+
Kind string `json:"kind"`
334+
}
335+
if json.Unmarshal(raw, &m) == nil && m.Kind != "" {
336+
return m.Kind
337+
}
338+
return "unknown"
339+
}
340+
341+
func truncate(s string, maxLen int) string {
342+
if len(s) <= maxLen {
343+
return s
344+
}
345+
return s[:maxLen-3] + "..."
346+
}

0 commit comments

Comments
 (0)