tpmjs/apps/omega-mac/OmegaMac/Views/Chat/MessageBubble.swift
Thomas Davis 1cd44b4e97 feat: add Omega Mac native macOS SwiftUI chat app
Native macOS counterpart to the web Omega agent, connecting directly
to the OpenAI API and TPMJS tool registry (1M+ AI-ready tools).

- SwiftUI app targeting macOS 15+ with dark theme
- Full agentic loop: auto-discover tools via BM25, stream OpenAI
  responses, execute tools via remote sandbox, loop up to 10x
- SwiftData persistence for conversations, messages, tool runs
- Keychain storage for API keys and environment variables
- SSE streaming via URLSession.bytes with custom parser
- Actor-based services (OpenAIService, TPMJSRegistryService)
- NavigationSplitView layout with sidebar + chat detail
- MarkdownUI for rendering assistant responses
- Settings: API key, model picker, env vars, custom system prompt
- Keyboard shortcuts: Cmd+N new chat, Cmd+, settings, Enter send

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:29:39 +10:00

69 lines
2 KiB
Swift

import SwiftUI
struct MessageBubble: View {
let message: Message
var body: some View {
switch message.role {
case .user:
userBubble
case .assistant:
assistantBubble
case .tool:
toolResultsBubble
case .system:
EmptyView()
}
}
private var userBubble: some View {
HStack(alignment: .top) {
Spacer(minLength: 60)
Text(message.content)
.font(.body)
.foregroundStyle(.white)
.padding(12)
.background(.accent)
.clipShape(RoundedRectangle(cornerRadius: 12))
.textSelection(.enabled)
}
.padding(.horizontal, 16)
}
private var assistantBubble: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
if !message.content.isEmpty {
MarkdownView(content: message.content)
}
// Token usage
if let input = message.inputTokens, let output = message.outputTokens {
Text("In: \(input) | Out: \(output)")
.font(.system(size: 10, design: .monospaced))
.foregroundStyle(.tertiary)
.padding(.top, 4)
}
}
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 12))
.textSelection(.enabled)
Spacer(minLength: 60)
}
.padding(.horizontal, 16)
}
private var toolResultsBubble: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 8) {
ForEach(message.toolCalls) { tc in
JSONToolResultView(toolCallData: tc)
}
}
Spacer(minLength: 60)
}
.padding(.horizontal, 16)
}
}