From 69571704a11239b61d4cb52d4cc9828deeec6554 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Fri, 11 Sep 2026 15:16:40 +0300 Subject: [PATCH] tool call vis improvement --- main.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index 6af0f30..601f970 100644 --- a/main.go +++ b/main.go @@ -834,6 +834,7 @@ type Message struct { ReasoningContent string `json:"reasoning_content,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` + ToolCallsShown bool `json:"-"` // tool call headers already printed live during streaming } type ToolCall struct { @@ -1133,6 +1134,28 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) { var lastUsage Usage tcs := map[int]*ToolCall{} var order []int + var toolCallsShown bool + + // Live, in-place display of tool calls as they stream in, so the user sees + // the invocation text being shaped instead of a blank pause. Only used when + // stdout is a terminal; piped/redirected output is left untouched. + tty := isTerminal(int(os.Stdout.Fd())) + liveIdx := -1 + liveActive := false + showLive := func() { + if !tty || len(order) == 0 { return } + cur := order[len(order)-1] + txt := fmt.Sprintf("[tool call: %s(%s)]", tcs[cur].Function.Name, filterText(tcs[cur].Function.Arguments)) + if liveActive && liveIdx != cur { + // A new tool call started; finalize the previous live line. + fmt.Print("\n") + liveActive = false + } + fmt.Print("\r\033[K" + c(txt, 33)) + liveActive = true + liveIdx = cur + toolCallsShown = true + } flushTable := func() { if len(tblBuf) > 0 { @@ -1199,16 +1222,32 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) { } } } - for _, tc := range dl.ToolCalls { - t, ok := tcs[tc.Index] - if !ok { - t = &ToolCall{Type: "function"} - tcs[tc.Index] = t - order = append(order, tc.Index) + if len(dl.ToolCalls) > 0 { + if inReasoning { + fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n") + inReasoning = false } - if tc.ID != "" { t.ID = tc.ID } - if tc.Function.Name != "" { t.Function.Name += tc.Function.Name } - if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments } + if liveIdx == -1 { + // First tool call of the stream: flush any pending content line so it + // is not overwritten by the live tool-call line. + flushTable() + if lineBuf != "" { + fmt.Println(renderMDLine(lineBuf, &mdSt)) + lineBuf = "" + } + } + for _, tc := range dl.ToolCalls { + t, ok := tcs[tc.Index] + if !ok { + t = &ToolCall{Type: "function"} + tcs[tc.Index] = t + order = append(order, tc.Index) + } + if tc.ID != "" { t.ID = tc.ID } + if tc.Function.Name != "" { t.Function.Name += tc.Function.Name } + if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments } + } + showLive() } } if err := ctx.Err(); err != nil { return Message{}, lastUsage, err } @@ -1221,10 +1260,16 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) { fmt.Println(renderMDLine(lineBuf, &mdSt)) } } + if liveActive { + // Finalize the in-progress tool-call line so following output starts fresh. + fmt.Print("\n") + liveActive = false + } if inReasoning { fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n") } m := Message{Role: "assistant"} + m.ToolCallsShown = toolCallsShown if content != "" { m.Content = strp(content) } if reas != "" { m.ReasoningContent = reas } if len(tcs) > 0 { @@ -1393,7 +1438,9 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error) for _, tc := range m.ToolCalls { if err := ctx.Err(); err != nil { return msgs, turnUsage, err } fn, astr := tc.Function.Name, filterText(tc.Function.Arguments) - fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33)) + if !m.ToolCallsShown { + fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33)) + } res, sty := "", 2 var a map[string]any if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {