This refactor eliminates unnecessary text flattening throughout the AiStudio4 system, ensuring that structured ContentBlocks maintain their integrity from creation through storage to display. The goal was to preserve tool execution data and other structured content so that conversations maintain their rich formatting when reloaded.
The system had well-designed ContentBlock infrastructure with proper typed content renderers on the frontend, but structured data was being destroyed at multiple points through unnecessary text flattening:
- LinearConvMessage used
string contentinstead ofContentBlock[] - MessageHistoryItem used
string Contentinstead ofContentBlock[] - SimpleChatResponse used
string ResponseTextinstead ofContentBlock[] - Multiple services were using
string.Join()to flatten ContentBlocks into plain text
When tool loops executed:
- ✅ Tools created proper ContentBlocks with specific types (Tool, ToolResponse, Text, etc.)
- ❌ Storage flattened the structure into plain strings
- ❌ Reload lost all structured data - everything became plain text
- ❌ Frontend couldn't render properly - lost rich formatting for tools, responses, etc.
The frontend contentBlockRendererRegistry was designed to handle structured ContentBlocks but never received them after reload.
// BEFORE
public class LinearConvMessage
{
public string role { get; set; }
public string content { get; set; } // ❌ Flattened
}
// AFTER
public class LinearConvMessage
{
public string role { get; set; }
public List<ContentBlock> contentBlocks { get; set; } = new List<ContentBlock>(); // ✅ Structured
}// BEFORE
public class MessageHistoryItem
{
public string Role { get; set; }
public string Content { get; set; } // ❌ Flattened
}
// AFTER
public class MessageHistoryItem
{
public string Role { get; set; }
public List<ContentBlock> ContentBlocks { get; set; } = new List<ContentBlock>(); // ✅ Structured
}// BEFORE
public class SimpleChatResponse
{
public bool Success { get; set; }
public string ResponseText { get; set; } // ❌ Flattened
}
// AFTER
public class SimpleChatResponse
{
public bool Success { get; set; }
public List<ContentBlock> ContentBlocks { get; set; } = new List<ContentBlock>(); // ✅ Structured
}// BEFORE - Flattening ContentBlocks
Content = string.Join("\n\n", msg.ContentBlocks?.Select(cb => cb.Content))
// AFTER - Preserving structure
ContentBlocks = msg.ContentBlocks ?? new List<ContentBlock>()Each AI service now handles ContentBlocks → API format conversion locally:
- OpenAI.cs: Converts ContentBlocks to OpenAI JSON format
- Claude.cs: Converts ContentBlocks to Claude JSON format
- NetOpenAi.cs: Converts ContentBlocks to .NET SDK format
- LlamaCpp.cs: Converts ContentBlocks to llama.cpp format
- Gemini.cs: Handles TTS text extraction from ContentBlocks
LinearConversationMessage.cs- Core data structureMessageHistoryItem.cs- History data structureSimpleChatResponse.cs- Response data structureDefaultChatService.cs- Service layer flattening removalToolResponseProcessor.cs- Message creation updatesOpenAI.cs- ContentBlocks → OpenAI API conversionNetOpenAi.cs- ContentBlocks → .NET SDK conversionClaude.cs- ContentBlocks → Claude API conversionLlamaCpp.cs- ContentBlocks → llama.cpp API conversionGemini.cs- ContentBlocks → Gemini API conversionMessageBuilder.cs- Message building with ContentBlocksAiServiceBase.cs- Base interjection message creation- Tools:
SecondAiOpinionTool.cs,ModifyFilesUsingMorph.cs,GeminiGoogleSearchTool.cs - Services:
SecondaryAiService.cs,ChatProcessingService.cs - Handlers:
ChatRequestHandler.cs
Now when tool loops execute and are reloaded:
- ✅ Execution: ContentBlocks created with proper types (Tool, ToolResponse, Text, etc.)
- ✅ Storage: ContentBlocks stored as structured data in v4BranchedConversation
- ✅ Reload: ContentBlocks loaded back with original structure intact
- ✅ Display: Frontend
contentBlockRendererRegistryrenders each type appropriately
The frontend infrastructure was already perfect - it just needed the structured data to not be destroyed:
- TextContentRenderer: Handles text blocks
- ToolContentRenderer: Handles tool execution blocks
- ToolResponseContentRenderer: Handles tool response blocks with rich formatting
- SystemContentRenderer: Handles system message blocks
- AiHiddenContentRenderer: Handles hidden AI content blocks
AI service providers maintain their existing API format requirements by converting ContentBlocks locally:
// Example: OpenAI service converts ContentBlocks to required JSON
foreach (var block in message.contentBlocks ?? new List<ContentBlock>())
{
if (block.ContentType == ContentType.Text)
{
messageContent.Add(new JObject
{
["type"] = "text",
["text"] = block.Content ?? ""
});
}
// Handle other content types...
}- Data Integrity: Structured content preserved throughout entire pipeline
- Rich Display: Tool responses, system messages, and other content display properly
- Persistence: Conversations maintain formatting after reload
- Extensibility: Easy to add new ContentBlock types with custom renderers
- Provider Flexibility: Each AI service controls its own format conversion
- Frontend Ready: Leverages existing
contentBlockRendererRegistryinfrastructure
- No frontend changes required - the infrastructure was already designed for this
- AI providers handle conversion locally - each service converts ContentBlocks to its required API format
- Tool execution data persists - rich tool responses now survive reload
- Backward compatibility maintained - existing functionality preserved while adding structure
This refactor transforms AiStudio4 from a text-flattening system to a structure-preserving one, enabling rich, persistent conversations that maintain their formatting and tool execution context across sessions.