llm and everything: Update ToolResult to use []Content instead of string for multimodal support

This was a journey. The sketch-generated summary below is acceptable,
but I want to tell you about it in my voice too. The goal was to send
screenshots to Claude, so that it could... look at them. Currently
the take screenshot and read screenshot tools are different, and they'll
need to be renamed/prompt-engineered a bit, but that's all fine.

The miserable part was that we had to change the return value
of tool from string to Content[], and this crosses several layers:
 - llm.Tool
 - llm.Content
 - ant.Content & openai and gemini friends
 - AgentMessage [we left this alone]

Extra fun is that Claude's API for sending images has nested Content
fields, and empty string and missing needs to be distinguished for the
Text field (because lots of shell commands return the empty string!).

For the UI, I made us transform the results into a string, dropping
images. This would have been yet more churn for not much obvious
benefit. Plus, it was going to break skaband's compatibility, and ...
yet more work.

OpenAI and Gemini don't obviously support images in this same way,
so they just don't get the tools.

~~~~~~~~~~ Sketch said:

This architectural change transforms tool results from plain strings to []Content arrays, enabling multimodal interaction in the system. Key changes include:

- Core structural changes:
  - Modified ToolResult type from string to []Content across all packages
  - Added MediaType field to Content struct for MIME type support
  - Created TextContent and ImageContent helper functions
  - Updated all tool.Run implementations to return []Content

- Image handling:
  - Implemented base64 image support in Anthropic adapter
  - Added proper media type detection and content formatting
  - Created browser_read_image tool for displaying screenshots
  - Updated browser_screenshot to provide usable image paths

- Adapter improvements:
  - Updated all LLM adapters (ANT, OAI, GEM) to handle content arrays
  - Added specialized image content handling in the Anthropic adapter
  - Ensured proper JSON serialization/deserialization for all content types
  - Improved test coverage for content arrays

- UI enhancements:
  - Added omitempty tags to reduce JSON response size
  - Updated TypeScript types to handle array content
  - Made field naming consistent (tool_error vs is_error)
  - Preserved backward compatibility for existing consumers

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s1a2b3c4d5e6f7g8h
diff --git a/claudetool/patch.go b/claudetool/patch.go
index 419e966..24a22ef 100644
--- a/claudetool/patch.go
+++ b/claudetool/patch.go
@@ -94,18 +94,18 @@
 }
 
 // PatchRun is the entry point for the user_patch tool.
-func PatchRun(ctx context.Context, m json.RawMessage) (string, error) {
+func PatchRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
 	var input patchInput
 	if err := json.Unmarshal(m, &input); err != nil {
-		return "", fmt.Errorf("failed to unmarshal user_patch input: %w", err)
+		return nil, fmt.Errorf("failed to unmarshal user_patch input: %w", err)
 	}
 
 	// Validate the input
 	if !filepath.IsAbs(input.Path) {
-		return "", fmt.Errorf("path %q is not absolute", input.Path)
+		return nil, fmt.Errorf("path %q is not absolute", input.Path)
 	}
 	if len(input.Patches) == 0 {
-		return "", fmt.Errorf("no patches provided")
+		return nil, fmt.Errorf("no patches provided")
 	}
 	// TODO: check whether the file is autogenerated, and if so, require a "force" flag to modify it.
 
@@ -118,11 +118,11 @@
 			switch patch.Operation {
 			case "prepend_bof", "append_eof", "overwrite":
 			default:
-				return "", fmt.Errorf("file %q does not exist", input.Path)
+				return nil, fmt.Errorf("file %q does not exist", input.Path)
 			}
 		}
 	case err != nil:
-		return "", fmt.Errorf("failed to read file %q: %w", input.Path, err)
+		return nil, fmt.Errorf("failed to read file %q: %w", input.Path, err)
 	}
 
 	likelyGoFile := strings.HasSuffix(input.Path, ".go")
@@ -151,7 +151,7 @@
 			buf.Replace(0, len(orig), patch.NewText)
 		case "replace":
 			if patch.OldText == "" {
-				return "", fmt.Errorf("patch %d: oldText cannot be empty for %s operation", i, patch.Operation)
+				return nil, fmt.Errorf("patch %d: oldText cannot be empty for %s operation", i, patch.Operation)
 			}
 
 			// Attempt to apply the patch.
@@ -214,7 +214,7 @@
 			patchErr = errors.Join(patchErr, fmt.Errorf("old text not found:\n%s", patch.OldText))
 			continue
 		default:
-			return "", fmt.Errorf("unrecognized operation %q", patch.Operation)
+			return nil, fmt.Errorf("unrecognized operation %q", patch.Operation)
 		}
 	}
 
@@ -224,18 +224,18 @@
 			"patches": input.Patches,
 			"errors":  patchErr,
 		})
-		return "", patchErr
+		return nil, patchErr
 	}
 
 	patched, err := buf.Bytes()
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	if err := os.MkdirAll(filepath.Dir(input.Path), 0o700); err != nil {
-		return "", fmt.Errorf("failed to create directory %q: %w", filepath.Dir(input.Path), err)
+		return nil, fmt.Errorf("failed to create directory %q: %w", filepath.Dir(input.Path), err)
 	}
 	if err := os.WriteFile(input.Path, patched, 0o600); err != nil {
-		return "", fmt.Errorf("failed to write patched contents to file %q: %w", input.Path, err)
+		return nil, fmt.Errorf("failed to write patched contents to file %q: %w", input.Path, err)
 	}
 
 	response := new(strings.Builder)
@@ -244,7 +244,7 @@
 	if parsed {
 		parseErr := parseGo(patched)
 		if parseErr != nil {
-			return "", fmt.Errorf("after applying all patches, the file no longer parses:\n%w", parseErr)
+			return nil, fmt.Errorf("after applying all patches, the file no longer parses:\n%w", parseErr)
 		}
 	}
 
@@ -253,7 +253,7 @@
 	}
 
 	// TODO: maybe report the patch result to the model, i.e. some/all of the new code after the patches and formatting.
-	return response.String(), nil
+	return llm.TextContent(response.String()), nil
 }
 
 func parseGo(buf []byte) error {