Merge pull request #11752 from cli/kw/run-view-pr-feedback

`gh agent-task view`: Follow-up to #11743
This commit is contained in:
Babak K. Shandiz 2025-09-17 10:00:50 +01:00 committed by GitHub
commit 6eafe9687c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 288 additions and 107 deletions

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"path/filepath"
"slices"
"strings"
@ -133,15 +134,16 @@ func renderLogEntry(entry chatCompletionChunkEntry, w io.Writer, io *iostreams.I
case "view":
args := viewToolArgs{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return false, fmt.Errorf("failed to parse 'view' tool call arguments: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to parse 'view' tool call arguments: %v\n", err)
continue
}
fmt.Fprintf(w, "View %s\n", cs.Bold(relativeFilePath(args.Path)))
renderToolCallTitle(w, cs, fmt.Sprintf("View %s", cs.Bold(relativeFilePath(args.Path))), "")
content := stripDiffFormat(choice.Delta.Content)
// TODO: Strip the diff formatting from this, but for now render as it is.
if err := renderFileContentAsMarkdown(args.Path, content, w, io); err != nil {
return false, fmt.Errorf("failed to render viewed file content: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render viewed file content: %v\n\n", err)
fmt.Fprintln(io.ErrOut, content) // raw fallback
}
case "bash":
if v := unmarshal[bashToolArgs](args); v != nil {
@ -153,10 +155,11 @@ func renderLogEntry(entry chatCompletionChunkEntry, w io.Writer, io *iostreams.I
contentWithCommand := choice.Delta.Content
if v.Command != "" {
contentWithCommand = fmt.Sprintf("%s\n%s", v.Command, choice.Delta.Content)
contentWithCommand = fmt.Sprintf("$ %s\n%s", v.Command, choice.Delta.Content)
}
if err := renderFileContentAsMarkdown("commands.sh", contentWithCommand, w, io); err != nil {
return false, fmt.Errorf("failed to render bash command output: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render bash command output: %v\n\n", err)
fmt.Fprintln(io.ErrOut, contentWithCommand)
}
}
// TODO: consider including more details for these bash-related tool calls.
@ -193,24 +196,26 @@ func renderLogEntry(entry chatCompletionChunkEntry, w io.Writer, io *iostreams.I
case "think":
args := thinkToolArgs{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return false, fmt.Errorf("failed to parse 'think' tool call arguments: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to parse 'think' tool call arguments: %v\n", err)
continue
}
// NOTE: omit the delta.content since it's the same as thought
renderToolCallTitle(w, cs, "Thought", "")
if err := renderRawMarkdown(args.Thought, w, io); err != nil {
return false, fmt.Errorf("failed to render thought: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render thought: %v\n", err)
}
case "report_progress":
args := reportProgressToolArgs{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return false, fmt.Errorf("failed to parse 'report_progress' tool call arguments: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to parse 'report_progress' tool call arguments: %v\n", err)
continue
}
renderToolCallTitle(w, cs, "Progress update", cs.Bold(args.CommitMessage))
if args.PrDescription != "" {
if err := renderRawMarkdown(args.PrDescription, w, io); err != nil {
return false, fmt.Errorf("failed to render PR description: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render PR description: %v\n", err)
}
}
@ -218,29 +223,33 @@ func renderLogEntry(entry chatCompletionChunkEntry, w io.Writer, io *iostreams.I
if choice.Delta.Content != "" {
// Try to treat this as JSON
if err := renderContentAsJSONMarkdown("", choice.Delta.Content, w, io); err != nil {
return false, fmt.Errorf("failed to render progress update content: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render progress update content: %v\n", err)
}
}
case "create":
args := createToolArgs{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return false, fmt.Errorf("failed to parse 'create' tool call arguments: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to parse 'create' tool call arguments: %v\n", err)
continue
}
renderToolCallTitle(w, cs, "Create", cs.Bold(relativeFilePath(args.Path)))
if err := renderFileContentAsMarkdown(args.Path, args.FileText, w, io); err != nil {
return false, fmt.Errorf("failed to render created file content: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render created file content: %v\n\n", err)
fmt.Fprintln(io.ErrOut, args.FileText)
}
case "str_replace":
args := strReplaceToolArgs{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return false, fmt.Errorf("failed to parse 'str_replace' tool call arguments: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to parse 'str_replace' tool call arguments: %v\n", err)
continue
}
renderToolCallTitle(w, cs, "Edit", cs.Bold(relativeFilePath(args.Path)))
if err := renderFileContentAsMarkdown("output.diff", choice.Delta.Content, w, io); err != nil {
return false, fmt.Errorf("failed to render str_replace diff: %w", err)
fmt.Fprintf(io.ErrOut, "\nfailed to render str_replace diff: %v\n\n", err)
fmt.Fprintln(io.ErrOut, choice.Delta.Content)
}
default:
// Unknown tool call. For example for "codeql_checker":
@ -333,14 +342,15 @@ func stripDiffFormat(diff string) string {
}
}
// If we found the hunk header end, we strip everything before it.
if hunkEndIndex != -1 {
lines = lines[hunkEndIndex+1:]
} else {
// This isn't a diff, so we defensively just return the original string.
// Guard clause: if we didn't find a hunk header, this isn't a diff, so
// we defensively just return the original string.
if hunkEndIndex == -1 {
return diff
}
// We found the hunk header end; strip everything before it.
lines = lines[hunkEndIndex+1:]
// Now we strip the leading + and - from lines, if they exist.
// Note: most of the time, but not all the time, we get a diff without
// these prefixes.
@ -358,10 +368,9 @@ func stripDiffFormat(diff string) string {
// renderFileContentAsMarkdown renders the given content as markdown
// based on the file extension of the path.
func renderFileContentAsMarkdown(path, content string, w io.Writer, io *iostreams.IOStreams) error {
parts := strings.Split(path, ".")
lang := parts[len(parts)-1]
lang := filepath.Ext(filepath.ToSlash(path))
if lang == "md" {
if lang == ".md" {
return renderRawMarkdown(content, w, io)
}
@ -422,62 +431,63 @@ func renderToolCallTitle(w io.Writer, cs *iostreams.ColorScheme, toolName, title
}
}
// genericToolCallNamesToTitles maps known generic tool call identifiers to human-friendly titles.
var genericToolCallNamesToTitles = map[string]string{
// Custom tools, the GitHub UI doesn't currently have these.
"codeql_checker": "Run CodeQL analysis",
// Playwright tools.
"playwright-browser_navigate": "Navigate Playwright web browser to a URL",
"playwright-browser_navigate_back": "Navigate back in Playwright web browser",
"playwright-browser_navigate_forward": "Navigate forward in Playwright web browser",
"playwright-browser_click": "Click element in Playwright web browser",
"playwright-browser_take_screenshot": "Take screenshot of Playwright web browser",
"playwright-browser_type": "Type in Playwright web browser",
"playwright-browser_wait_for": "Wait for text to appear/disappear in Playwright web browser",
"playwright-browser_evaluate": "Run JavaScript in Playwright web browser",
"playwright-browser_snapshot": "Take snapshot of page in Playwright web browser",
"playwright-browser_resize": "Resize Playwright web browser window",
"playwright-browser_close": "Close Playwright web browser",
"playwright-browser_press_key": "Press key in Playwright web browser",
"playwright-browser_select_option": "Select option in Playwright web browser",
"playwright-browser_handle_dialog": "Interact with dialog in Playwright web browser",
"playwright-browser_console_messages": "Get console messages from Playwright web browser",
"playwright-browser_drag": "Drag mouse between elements in Playwright web browser",
"playwright-browser_file_upload": "Upload file in Playwright web browser",
"playwright-browser_hover": "Hover mouse over element in Playwright web browser",
"playwright-browser_network_requests": "Get network requests from Playwright web browser",
// GitHub MCP server common tools
"github-mcp-server-get_file_contents": "Get file contents from GitHub",
"github-mcp-server-get_pull_request": "Get pull request from GitHub",
"github-mcp-server-get_issue": "Get issue from GitHub",
"github-mcp-server-get_pull_request_files": "Get pull request changed files from GitHub",
"github-mcp-server-list_pull_requests": "List pull requests on GitHub",
"github-mcp-server-list_branches": "List branches on GitHub",
"github-mcp-server-get_pull_request_diff": "Get pull request diff from GitHub",
"github-mcp-server-get_pull_request_comments": "Get pull request comments from GitHub",
"github-mcp-server-get_commit": "Get commit from GitHub",
"github-mcp-server-search_repositories": "Search repositories on GitHub",
"github-mcp-server-search_code": "Search code on GitHub",
"github-mcp-server-get_issue_comments": "Get issue comments from GitHub",
"github-mcp-server-list_issues": "List issues on GitHub",
"github-mcp-server-search_pull_requests": "Search pull requests on GitHub",
"github-mcp-server-list_commits": "List commits on GitHub",
"github-mcp-server-get_pull_request_status": "Get pull request status from GitHub",
"github-mcp-server-search_issues": "Search issues on GitHub",
"github-mcp-server-get_pull_request_reviews": "Get pull request reviews from GitHub",
"github-mcp-server-download_workflow_run_artifact": "Download GitHub Actions workflow run artifact",
"github-mcp-server-get_job_logs": "Get GitHub Actions job logs",
"github-mcp-server-get_workflow_run": "Get GitHub Actions workflow run",
"github-mcp-server-get_workflow_run_logs": "Get GitHub Actions workflow run logs",
"github-mcp-server-get_workflow_run_usage": "Get GitHub Actions workflow usage",
"github-mcp-server-list_workflow_jobs": "List GitHub Actions workflow jobs",
"github-mcp-server-list_workflow_run_artifacts": "List GitHub Actions workflow run artifacts",
"github-mcp-server-list_workflow_runs": "List GitHub Actions workflow runs",
"github-mcp-server-list_workflows": "List GitHub Actions workflows",
}
func renderGenericToolCall(w io.Writer, cs *iostreams.ColorScheme, name string) {
genericToolCallNamesToTitles := map[string]string{
// Custom tools, the GitHub UI doesn't currently have these.
"codeql_checker": "Run CodeQL analysis",
// Playwright tools.
"playwright-browser_navigate": "Navigate Playwright web browser to a URL",
"playwright-browser_navigate_back": "Navigate back in Playwright web browser",
"playwright-browser_navigate_forward": "Navigate forward in Playwright web browser",
"playwright-browser_click": "Click element in Playwright web browser",
"playwright-browser_take_screenshot": "Take screenshot of Playwright web browser",
"playwright-browser_type": "Type in Playwright web browser",
"playwright-browser_wait_for": "Wait for text to appear/disappear in Playwright web browser",
"playwright-browser_evaluate": "Run JavaScript in Playwright web browser",
"playwright-browser_snapshot": "Take snapshot of page in Playwright web browser",
"playwright-browser_resize": "Resize Playwright web browser window",
"playwright-browser_close": "Close Playwright web browser",
"playwright-browser_press_key": "Press key in Playwright web browser",
"playwright-browser_select_option": "Select option in Playwright web browser",
"playwright-browser_handle_dialog": "Interact with dialog in Playwright web browser",
"playwright-browser_console_messages": "Get console messages from Playwright web browser",
"playwright-browser_drag": "Drag mouse between elements in Playwright web browser",
"playwright-browser_file_upload": "Upload file in Playwright web browser",
"playwright-browser_hover": "Hover mouse over element in Playwright web browser",
"playwright-browser_network_requests": "Get network requests from Playwright web browser",
// GitHub MCP server common tools
"github-mcp-server-get_file_contents": "Get file contents from GitHub",
"github-mcp-server-get_pull_request": "Get pull request from GitHub",
"github-mcp-server-get_issue": "Get issue from GitHub",
"github-mcp-server-get_pull_request_files": "Get pull request changed files from GitHub",
"github-mcp-server-list_pull_requests": "List pull requests on GitHub",
"github-mcp-server-list_branches": "List branches on GitHub",
"github-mcp-server-get_pull_request_diff": "Get pull request diff from GitHub",
"github-mcp-server-get_pull_request_comments": "Get pull request comments from GitHub",
"github-mcp-server-get_commit": "Get commit from GitHub",
"github-mcp-server-search_repositories": "Search repositories on GitHub",
"github-mcp-server-search_code": "Search code on GitHub",
"github-mcp-server-get_issue_comments": "Get issue comments from GitHub",
"github-mcp-server-list_issues": "List issues on GitHub",
"github-mcp-server-search_pull_requests": "Search pull requests on GitHub",
"github-mcp-server-list_commits": "List commits on GitHub",
"github-mcp-server-get_pull_request_status": "Get pull request status from GitHub",
"github-mcp-server-search_issues": "Search issues on GitHub",
"github-mcp-server-get_pull_request_reviews": "Get pull request reviews from GitHub",
"github-mcp-server-download_workflow_run_artifact": "Download GitHub Actions workflow run artifact",
"github-mcp-server-get_job_logs": "Get GitHub Actions job logs",
"github-mcp-server-get_workflow_run": "Get GitHub Actions workflow run",
"github-mcp-server-get_workflow_run_logs": "Get GitHub Actions workflow run logs",
"github-mcp-server-get_workflow_run_usage": "Get GitHub Actions workflow usage",
"github-mcp-server-list_workflow_jobs": "List GitHub Actions workflow jobs",
"github-mcp-server-list_workflow_run_artifacts": "List GitHub Actions workflow run artifacts",
"github-mcp-server-list_workflow_runs": "List GitHub Actions workflow runs",
"github-mcp-server-list_workflows": "List GitHub Actions workflows",
}
toolName, ok := genericToolCallNamesToTitles[name]
if !ok {
toolName = fmt.Sprintf("Call to %s", name)

View file

@ -13,19 +13,26 @@ import (
func TestFollow(t *testing.T) {
tests := []struct {
name string
log string
want string
name string
log string
wantStdoutFile string
wantStderrFile string
}{
{
name: "sample log 1",
log: "testdata/log-1-input.txt",
want: "testdata/log-1-want.txt",
name: "sample log 1",
log: "testdata/log-1-input.txt",
wantStdoutFile: "testdata/log-1-want.txt",
},
{
name: "sample log 2",
log: "testdata/log-2-input.txt",
want: "testdata/log-2-want.txt",
name: "sample log 2",
log: "testdata/log-2-input.txt",
wantStdoutFile: "testdata/log-2-want.txt",
},
{
name: "sample log 3 (tolerant parse failures)",
log: "testdata/log-3-synthetic-failures-input.txt",
wantStdoutFile: "testdata/log-3-synthetic-failures-want.txt",
wantStderrFile: "testdata/log-3-synthetic-failures-want-stderr.txt",
},
}
@ -34,7 +41,7 @@ func TestFollow(t *testing.T) {
raw, err := os.ReadFile(tt.log)
require.NoError(t, err)
// Delete all the `/r` to make the tests OS-agnostic.
// Normalize CRLF to LF to make the tests OS-agnostic.
raw = []byte(strings.ReplaceAll(string(raw), "\r\n", "\n"))
lines := slices.DeleteFunc(strings.Split(string(raw), "\n"), func(line string) bool {
@ -50,7 +57,7 @@ func TestFollow(t *testing.T) {
return []byte(strings.Join(lines[0:hits], "\n\n")), nil
}
ios, _, stdout, _ := iostreams.Test()
ios, _, stdout, stderr := iostreams.Test()
err = NewLogRenderer().Follow(fetcher, stdout, ios)
require.NoError(t, err)
@ -58,15 +65,31 @@ func TestFollow(t *testing.T) {
// Handy note for updating the testdata files when they change:
// ext := filepath.Ext(tt.log)
// stripped := strings.TrimSuffix(tt.log, ext)
// os.WriteFile(stripped+".want"+ext, stdout.Bytes(), 0644)
// stripped = strings.TrimSuffix(stripped, "-input")
// os.WriteFile(stripped+"-want"+ext, stdout.Bytes(), 0644)
// if tt.wantStderrFile != "" {
// os.WriteFile(stripped+"-want-stderr"+ext, stderr.Bytes(), 0644)
// }
want, err := os.ReadFile(tt.want)
wantStdout, err := os.ReadFile(tt.wantStdoutFile)
require.NoError(t, err)
// Delete all the `/r` to make the tests OS-agnostic.
want = []byte(strings.ReplaceAll(string(want), "\r\n", "\n"))
// Normalize CRLF to LF to make the tests OS-agnostic.
wantStdout = []byte(strings.ReplaceAll(string(wantStdout), "\r\n", "\n"))
assert.Equal(t, string(want), stdout.String())
assert.Equal(t, string(wantStdout), stdout.String())
if tt.wantStderrFile != "" {
wantStderr, err := os.ReadFile(tt.wantStderrFile)
require.NoError(t, err)
// Normalize CRLF to LF to make the tests OS-agnostic.
wantStderr = []byte(strings.ReplaceAll(string(wantStderr), "\r\n", "\n"))
assert.Equal(t, string(wantStderr), stderr.String())
} else {
require.Empty(t, stderr, "expected no stderr output")
}
})
}
}

View file

@ -11,7 +11,7 @@ View repository
README.md
Bash: Check current directory and list all files
cd /home/runner/work/foo/foo && pwd && ls -la
$ cd /home/runner/work/foo/foo && pwd && ls -la
/home/runner/work/foo/foo
total 20
drwxr-xr-x 4 runner docker 4096 Sep 9 10:17 .
@ -36,8 +36,8 @@ View README.md
# foo
Bash: Check if there are any existing scripts or build files
cd /home/runner/work/foo/foo && find . -name "*.sh" -o -name "Makefile" -o -
name "package.json" -o -name "*.yml" -o -name "*.yaml"
$ cd /home/runner/work/foo/foo && find . -name "*.sh" -o -name "Makefile" -
o -name "package.json" -o -name "*.yml" -o -name "*.yaml"
./.github/workflows/upload-artifact.yml
./.github/workflows/manual-dispatch.yml
./.github/workflows/cache.yml
@ -195,7 +195,7 @@ Create: os_info.sh
Now let me make the script executable and test it:
Bash: Make script executable and test it
cd /home/runner/work/foo/foo && chmod +x os_info.sh && ./os_info.sh
$ cd /home/runner/work/foo/foo && chmod +x os_info.sh && ./os_info.sh
==================================================
OPERATING SYSTEM INFORMATION
==================================================
@ -270,7 +270,7 @@ diff --git a/home/runner/work/foo/foo/README.md
Let me verify the changes look good:
Bash: Check file permissions and final status
cd /home/runner/work/foo/foo && ls -la os_info.sh && echo "---" && git
$ cd /home/runner/work/foo/foo && ls -la os_info.sh && echo "---" && git
status
-rwxr-xr-x 1 runner docker 2139 Sep 9 10:18 os_info.sh
---
@ -323,7 +323,7 @@ Perfect! Let me test the script one more time to make sure everything is
working correctly and commit the changes:
Bash: Final test of the script
cd /home/runner/work/foo/foo && echo "Testing script execution:" && .
$ cd /home/runner/work/foo/foo && echo "Testing script execution:" && .
/os_info.sh | head -10
Testing script execution:
==================================================

View file

@ -6,7 +6,7 @@ I'll help you create a .yaml file with random fields in the repository. Let
and then create a plan.
Bash: Find documentation and config files to understand the repository
cd /home/runner/work/foo/foo && find . -type f -name "*.md" -o -name "*.
$ cd /home/runner/work/foo/foo && find . -type f -name "*.md" -o -name "*.
txt" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" | head -20
./README.md
./.github/workflows/upload-artifact.yml
@ -23,7 +23,7 @@ View repository
README.md
Bash: List all files in the root directory
cd /home/runner/work/foo/foo && ls -la
$ cd /home/runner/work/foo/foo && ls -la
total 20
drwxr-xr-x 4 runner runner 4096 Sep 11 13:35 .
drwxr-xr-x 3 runner runner 4096 Sep 11 13:34 ..
@ -37,7 +37,7 @@ View README.md
# foo
Bash: Check git status to understand current state
cd /home/runner/work/foo/foo && git --no-pager status
$ cd /home/runner/work/foo/foo && git --no-pager status
On branch copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25
Your branch is up to date with 'origin/copilot/fix-5b1c2fcb-9248-492c-84fa-
04a3e0304d25'.
@ -197,13 +197,13 @@ Now let me verify that the YAML file is valid by testing it with a YAML
parser:
Bash: Validate the YAML file syntax
cd /home/runner/work/foo/foo && python3 -c "import yaml; yaml.
$ cd /home/runner/work/foo/foo && python3 -c "import yaml; yaml.
safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!')"
<exited with exit code 0>
Bash: Validate YAML and show structure
cd /home/runner/work/foo/foo && python3 -c "import yaml; data = yaml.
$ cd /home/runner/work/foo/foo && python3 -c "import yaml; data = yaml.
safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!');
print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')"
@ -213,7 +213,7 @@ cd /home/runner/work/foo/foo && python3 -c "import yaml; data = yaml.
Let me try a different approach to verify the YAML:
Bash: Detailed YAML validation with content verification
cd /home/runner/work/foo/foo && python3 -c "
$ cd /home/runner/work/foo/foo && python3 -c "
import yaml
try:
with open('sample-data.yaml', 'r') as f:
@ -237,7 +237,7 @@ cd /home/runner/work/foo/foo && python3 -c "
Perfect! Let me also check the file contents and verify the current status:
Bash: Check git status to see the new file
cd /home/runner/work/foo/foo && git --no-pager status
$ cd /home/runner/work/foo/foo && git --no-pager status
On branch copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25
Your branch is up to date with 'origin/copilot/fix-5b1c2fcb-9248-492c-84fa-
04a3e0304d25'.

View file

@ -0,0 +1,27 @@
data: {"id": "bad1", "object": "chat.completion.chunk", "choices": [ { "delta": { "tool_calls": [ { "function": { "name": "view", "arguments": "{bad json" } } ] } } ] }
data: {"id":"v1","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line\nINSIDE A VIEW CALL","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md\"}"},"id":"tc1","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"v1b","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md\"}"},"id":"tc1b","index":0}]}],"finish_reason":"tool_calls","index":0}]}
data: {"id":"v1","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line\nINSIDE A VIEW CALL","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md"},"id":"tc1","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"t1","object":"chat.completion.chunk","choices":[{"delta":{"content":"THINK","tool_calls":[{"function":{"name":"think","arguments":"{\"thought\":123"},"id":"tc2","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"t2","object":"chat.completion.chunk","choices":[{"delta":{"content":"A valid thought to render.","reasoning_text":"Interim reasoning that should show as raw markdown.","tool_calls":[{"function":{"name":"think","arguments":"{\"thought\":\"A valid thought to render.\"}"},"id":"tc3","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"rp1","object":"chat.completion.chunk","choices":[{"delta":{"content":"RP","tool_calls":[{"function":{"name":"report_progress","arguments":"{\"commitMessage\": 5"},"id":"tc4","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"rp2","object":"chat.completion.chunk","choices":[{"delta":{"content":"not-json","tool_calls":[{"function":{"name":"report_progress","arguments":"{\"commitMessage\":\"Valid commit msg\"}"},"id":"tc5","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"c1","object":"chat.completion.chunk","choices":[{"delta":{"content":"CREATE","tool_calls":[{"function":{"name":"create","arguments":"{\"path\":\"/abs/path/file.txt\""},"id":"tc6","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"c2","object":"chat.completion.chunk","choices":[{"delta":{"content":"CREATE2","tool_calls":[{"function":{"name":"create","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/new.txt\",\"file_text\":\"hello world\"}"},"id":"tc7","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"sr1","object":"chat.completion.chunk","choices":[{"delta":{"content":"SR","tool_calls":[{"function":{"name":"str_replace","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/file.diff"},"id":"tc8","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"sr2","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line","tool_calls":[{"function":{"name":"str_replace","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/file.diff\"}"},"id":"tc9","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"u1","object":"chat.completion.chunk","choices":[{"delta":{"content":"{\"foo\":1}","tool_calls":[{"function":{"name":"mystery_tool","arguments":"{\"bar\":2}"},"id":"tc10","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"end","object":"chat.completion.chunk","choices":[{"delta":{"content":"","tool_calls":[],"role":"assistant"},"finish_reason":"stop","index":0}]}

View file

@ -0,0 +1,27 @@
data: {"id": "bad1", "object": "chat.completion.chunk", "choices": [ { "delta": { "tool_calls": [ { "function": { "name": "view", "arguments": "{bad json" } } ] } } ] }
data: {"id":"v1","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line\nINSIDE A VIEW CALL","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md\"}"},"id":"tc1","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"v1b","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md\"}"},"id":"tc1b","index":0}]}],"finish_reason":"tool_calls","index":0}]}
data: {"id":"v1","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line\nINSIDE A VIEW CALL","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md"},"id":"tc1","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"t1","object":"chat.completion.chunk","choices":[{"delta":{"content":"THINK","tool_calls":[{"function":{"name":"think","arguments":"{\"thought\":123"},"id":"tc2","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"t2","object":"chat.completion.chunk","choices":[{"delta":{"content":"A valid thought to render.","reasoning_text":"Interim reasoning that should show as raw markdown.","tool_calls":[{"function":{"name":"think","arguments":"{\"thought\":\"A valid thought to render.\"}"},"id":"tc3","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"rp1","object":"chat.completion.chunk","choices":[{"delta":{"content":"RP","tool_calls":[{"function":{"name":"report_progress","arguments":"{\"commitMessage\": 5"},"id":"tc4","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"rp2","object":"chat.completion.chunk","choices":[{"delta":{"content":"not-json","tool_calls":[{"function":{"name":"report_progress","arguments":"{\"commitMessage\":\"Valid commit msg\"}"},"id":"tc5","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"c1","object":"chat.completion.chunk","choices":[{"delta":{"content":"CREATE","tool_calls":[{"function":{"name":"create","arguments":"{\"path\":\"/abs/path/file.txt\""},"id":"tc6","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"c2","object":"chat.completion.chunk","choices":[{"delta":{"content":"CREATE2","tool_calls":[{"function":{"name":"create","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/new.txt\",\"file_text\":\"hello world\"}"},"id":"tc7","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"sr1","object":"chat.completion.chunk","choices":[{"delta":{"content":"SR","tool_calls":[{"function":{"name":"str_replace","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/file.diff"},"id":"tc8","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"sr2","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line","tool_calls":[{"function":{"name":"str_replace","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/file.diff\"}"},"id":"tc9","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"u1","object":"chat.completion.chunk","choices":[{"delta":{"content":"{\"foo\":1}","tool_calls":[{"function":{"name":"mystery_tool","arguments":"{\"bar\":2}"},"id":"tc10","index":0}]},"finish_reason":"tool_calls","index":0}]}
data: {"id":"end","object":"chat.completion.chunk","choices":[{"delta":{"content":"","tool_calls":[],"role":"assistant"},"finish_reason":"stop","index":0}]}

View file

@ -0,0 +1,10 @@
failed to parse 'view' tool call arguments: unexpected end of JSON input
failed to parse 'think' tool call arguments: unexpected end of JSON input
failed to parse 'report_progress' tool call arguments: unexpected end of JSON input
failed to parse 'create' tool call arguments: unexpected end of JSON input
failed to parse 'str_replace' tool call arguments: unexpected end of JSON input

View file

@ -0,0 +1,39 @@
View repo/README.md
old line
new line
unchanged line
INSIDE A VIEW CALL
Interim reasoning that should show as raw markdown.
Thought
A valid thought to render.
Progress update: Valid commit msg
Create: repo/new.txt
hello world
Edit: repo/file.diff
@@ -1,2 +1,2 @@
-old line
+new line
unchanged line
Call to mystery_tool
Output:
{
"foo": 1
}
Input:
{
"bar": 2
}

View file

@ -0,0 +1,39 @@
View repo/README.md
old line
new line
unchanged line
INSIDE A VIEW CALL
Interim reasoning that should show as raw markdown.
Thought
A valid thought to render.
Progress update: Valid commit msg
Create: repo/new.txt
hello world
Edit: repo/file.diff
@@ -1,2 +1,2 @@
-old line
+new line
unchanged line
Call to mystery_tool
Output:
{
"foo": 1
}
Input:
{
"bar": 2
}

View file

@ -325,6 +325,12 @@ func printLogs(opts *ViewOptions, capiClient capi.CapiClient, sessionID string)
renderer := opts.LogRenderer()
if err := opts.IO.StartPager(); err == nil {
defer opts.IO.StopPager()
} else {
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
}
if opts.Follow {
var called bool
fetcher := func() ([]byte, error) {