Fix duplicate content and loading message in sandbox output

- Fix Claude CLI output parsing: only extract content from final
  assistant message (with stop_reason) to avoid duplicate content
- Remove trailing "..." from sandbox loading message

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Max 2026-01-31 15:39:26 +08:00
parent bec8d9d426
commit 45963137c6
2 changed files with 35 additions and 30 deletions

View file

@ -86,7 +86,7 @@ func (ast *Assistant) initSandbox(ctx *context.Context, opts *context.Options) (
loadingMsg := &message.Message{
Type: message.TypeLoading,
Props: map[string]interface{}{
"message": "Preparing sandbox environment...",
"message": "Preparing sandbox environment",
},
}
loadingMsgID, _ := ctx.SendStream(loadingMsg)

View file

@ -327,52 +327,57 @@ func (e *Executor) parseStream(reader io.Reader, handler message.StreamFunc) (*a
case "assistant":
// Assistant message - extract content
// Note: Claude CLI sends multiple assistant messages, each with cumulative content.
// We only process the final one (with stop_reason) to avoid duplicates.
if msgData, ok := msg["message"].(map[string]interface{}); ok {
// Get model from message
if m, ok := msgData["model"].(string); ok && model == "" {
model = m
}
// Extract content array
if contentArr, ok := msgData["content"].([]interface{}); ok {
for _, item := range contentArr {
if contentItem, ok := item.(map[string]interface{}); ok {
itemType, _ := contentItem["type"].(string)
// Check if this is the final message (has stop_reason)
stopReason, hasStopReason := msgData["stop_reason"].(string)
isFinalMessage := hasStopReason && stopReason != ""
switch itemType {
case "text":
if text, ok := contentItem["text"].(string); ok {
// Only write if this is new content (avoid duplicates)
if textContent.Len() == 0 || !strings.HasSuffix(textContent.String(), text) {
// Extract content array - only from final message to avoid duplicates
if isFinalMessage {
if contentArr, ok := msgData["content"].([]interface{}); ok {
for _, item := range contentArr {
if contentItem, ok := item.(map[string]interface{}); ok {
itemType, _ := contentItem["type"].(string)
switch itemType {
case "text":
if text, ok := contentItem["text"].(string); ok {
textContent.WriteString(text)
// Send to stream handler if available
if handler != nil {
handler(message.ChunkText, []byte(text))
}
}
// Send to stream handler if available
if handler != nil {
handler(message.ChunkText, []byte(text))
}
}
case "tool_use":
toolCall := agentContext.ToolCall{
ID: getString(contentItem, "id"),
Type: agentContext.ToolTypeFunction,
Function: agentContext.Function{
Name: getString(contentItem, "name"),
},
}
// Get input as JSON string
if input, ok := contentItem["input"]; ok {
if inputJSON, err := json.Marshal(input); err == nil {
toolCall.Function.Arguments = string(inputJSON)
case "tool_use":
toolCall := agentContext.ToolCall{
ID: getString(contentItem, "id"),
Type: agentContext.ToolTypeFunction,
Function: agentContext.Function{
Name: getString(contentItem, "name"),
},
}
// Get input as JSON string
if input, ok := contentItem["input"]; ok {
if inputJSON, err := json.Marshal(input); err == nil {
toolCall.Function.Arguments = string(inputJSON)
}
}
toolCalls = append(toolCalls, toolCall)
}
toolCalls = append(toolCalls, toolCall)
}
}
}
}
// Extract usage
// Extract usage (from any message that has it)
if usageData, ok := msgData["usage"].(map[string]interface{}); ok {
usage = &message.UsageInfo{}
if v, ok := usageData["input_tokens"].(float64); ok {