Skip to content

Commit 67b9d07

Browse files
intel352claude
andcommitted
fix: interactive PTY prompt detection for Copilot + response extraction
- Copilot prompt detection: match ❯ with "Type @" placeholder (not bare >) - Copilot response end: detect ● (assistant marker) + new ❯ prompt - extractResponse: Strategy 1 looks for ● markers (Copilot), Strategy 2 looks for text between ❯ lines (Claude Code) - Trust handler: expanded to match Copilot's "Confirm folder trust" dialog - Skips dialog screens before checking for input prompt Tested manually with real Copilot CLI: trust auto-dismissed, multi-turn conversation working (5+5=10, multiply by 3=30, 2 second response time). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 886a836 commit 67b9d07

2 files changed

Lines changed: 46 additions & 20 deletions

File tree

genkit/pty_adapters.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,21 +137,20 @@ func (CopilotCLIAdapter) HealthCheckArgs() []string {
137137
return []string{"-p", "say ok"}
138138
}
139139

140-
var copilotPromptRegex = regexp.MustCompile(`(?m)^>\s`)
140+
// copilotPromptRegex matches the Copilot input prompt: ❯ followed by placeholder text.
141+
// The current prompt has "Type @" in it; prior input prompts are greyed ❯ with user text.
142+
var copilotPromptRegex = regexp.MustCompile(`❯\s+(Type @|$)`)
141143

142144
func (CopilotCLIAdapter) DetectPrompt(output string) bool {
143-
clean := stripANSI(output)
144-
return copilotPromptRegex.MatchString(clean)
145+
// The input prompt shows "❯ Type @..." — this is distinct from greyed prior-input ❯ lines.
146+
return copilotPromptRegex.MatchString(output)
145147
}
146148

147149
func (CopilotCLIAdapter) DetectResponseEnd(output string) bool {
148-
clean := stripANSI(output)
149-
locs := copilotPromptRegex.FindAllStringIndex(clean, -1)
150-
if len(locs) < 2 {
151-
return false
152-
}
153-
between := clean[locs[0][1]:locs[1][0]]
154-
return strings.TrimSpace(between) != ""
150+
// Response is complete when we see ● (assistant marker) followed by a new ❯ prompt.
151+
hasResponse := strings.Contains(output, "●")
152+
hasNewPrompt := copilotPromptRegex.MatchString(output)
153+
return hasResponse && hasNewPrompt
155154
}
156155

157156
func (CopilotCLIAdapter) ParseResponse(raw string) string {

genkit/pty_provider.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,21 @@ func (p *ptyProvider) waitForPrompt(ctx context.Context, ptmx *os.File, deadline
224224
}
225225

226226
screen := p.vt.String()
227+
lower := strings.ToLower(screen)
227228

228-
// Auto-handle trust prompts (e.g., "trust this folder" in Claude Code)
229-
if strings.Contains(screen, "trust") && strings.Contains(screen, "Yes") {
229+
// Auto-handle trust/safety prompts before checking for the actual input prompt.
230+
// Both Claude Code and Copilot show trust dialogs on first use in a directory.
231+
if (strings.Contains(lower, "trust") || strings.Contains(lower, "safety check")) &&
232+
(strings.Contains(screen, "Yes") || strings.Contains(screen, "Enter to confirm") || strings.Contains(screen, "Enter to select")) {
230233
ptmx.Write([]byte{'\r'})
231-
time.Sleep(1 * time.Second)
234+
time.Sleep(2 * time.Second)
235+
continue
236+
}
237+
238+
// Only detect the actual input prompt if we're NOT in a trust/dialog screen.
239+
if strings.Contains(screen, "Confirm folder") || strings.Contains(screen, "safety check") {
240+
// Still in a dialog — wait
241+
time.Sleep(500 * time.Millisecond)
232242
continue
233243
}
234244

@@ -284,13 +294,34 @@ func (p *ptyProvider) readResponse(ctx context.Context, ptmx *os.File, deadline
284294
}
285295
}
286296

287-
// extractResponse extracts the assistant's response text from the virtual terminal screen.
288-
// It looks for text between the user's message and the next prompt indicator.
297+
// extractResponse extracts the most recent assistant response from the virtual terminal screen.
298+
// Handles multiple output formats:
299+
// - Claude Code: response text appears between greyed ❯ (user input) and new ❯ prompt
300+
// - Copilot: responses are prefixed with ● (bullet marker)
289301
func (p *ptyProvider) extractResponse(screen string) string {
290302
lines := strings.Split(screen, "\n")
291303
var response []string
292-
inResponse := false
293304

305+
// Strategy 1: Look for ● (Copilot-style response markers).
306+
// Collect all ● lines as response text.
307+
for _, line := range lines {
308+
trimmed := strings.TrimSpace(line)
309+
if strings.HasPrefix(trimmed, "●") && !strings.Contains(trimmed, "💡") && !strings.Contains(trimmed, "Environment") {
310+
// Remove the ● prefix
311+
text := strings.TrimSpace(strings.TrimPrefix(trimmed, "●"))
312+
if text != "" {
313+
response = append(response, text)
314+
}
315+
}
316+
}
317+
if len(response) > 0 {
318+
// Return the LAST response (most recent turn)
319+
return response[len(response)-1]
320+
}
321+
322+
// Strategy 2: Look for text between greyed ❯ and the next ❯ prompt (Claude Code style).
323+
inResponse := false
324+
response = nil
294325
for _, line := range lines {
295326
trimmed := strings.TrimSpace(line)
296327

@@ -304,10 +335,6 @@ func (p *ptyProvider) extractResponse(screen string) string {
304335

305336
// Skip horizontal rules (box-drawing chars)
306337
if len(trimmed) > 5 && strings.Count(trimmed, "─") > len(trimmed)/2 {
307-
if inResponse {
308-
// A horizontal rule after response content likely means end of response area
309-
continue
310-
}
311338
continue
312339
}
313340

0 commit comments

Comments
 (0)