feat(tools): stream shell_run output live into the TUI
This commit is contained in:
+15
-3
@@ -683,15 +683,27 @@ func (m *model) mergeToolLog(text string) bool {
|
||||
if incoming.output == "" {
|
||||
return false
|
||||
}
|
||||
incomingHeader, _, ok := strings.Cut(text, "\n[output]\n")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for i := len(m.messages) - 1; i >= 0; i-- {
|
||||
if m.messages[i].role != roleTool {
|
||||
continue
|
||||
}
|
||||
existing := parseToolLog(m.messages[i].content)
|
||||
if existing.name == incoming.name && existing.args == incoming.args && existing.output == "" {
|
||||
m.messages[i].content = text
|
||||
return true
|
||||
if existing.name != incoming.name || existing.args != incoming.args {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(m.messages[i].content, "\n[output]\n") {
|
||||
// Placeholder card from the tool-start log; first chunk fills it.
|
||||
m.messages[i].content = text
|
||||
} else {
|
||||
// Streaming tools send incremental chunks; append the raw delta
|
||||
// so line boundaries are preserved inside the same card.
|
||||
m.messages[i].content += strings.TrimPrefix(text, incomingHeader+"\n[output]\n")
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -395,6 +395,28 @@ func TestToolLogOutputUpdatesExistingToolCard(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolLogStreamingChunksAppendToSameCard(t *testing.T) {
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
||||
m.messages = append(m.messages, message{role: roleTool, content: `shell_run echo hi`})
|
||||
|
||||
if !m.mergeToolLog("shell_run echo hi\n[output]\none\n") {
|
||||
t.Fatal("expected first mergeToolLog to return true")
|
||||
}
|
||||
if !m.mergeToolLog("shell_run echo hi\n[output]\ntwo\n") {
|
||||
t.Fatal("expected second mergeToolLog to return true")
|
||||
}
|
||||
if len(m.messages) != 1 {
|
||||
t.Fatalf("messages count = %d, want 1", len(m.messages))
|
||||
}
|
||||
plain := stripANSI(m.renderMessages())
|
||||
if !strings.Contains(plain, "one") || !strings.Contains(plain, "two") {
|
||||
t.Fatalf("rendered output missing streamed lines:\n%s", plain)
|
||||
}
|
||||
if strings.Contains(plain, "onetwo") {
|
||||
t.Fatalf("streamed lines were concatenated:\n%s", plain)
|
||||
}
|
||||
}
|
||||
|
||||
type tuiCompactProvider struct{}
|
||||
|
||||
func (tuiCompactProvider) ChatStream(ctx context.Context, req llm.ChatRequest, emit func(llm.StreamEvent) error) error {
|
||||
|
||||
Reference in New Issue
Block a user