tpmjs/apps/omega-mac/OmegaMac/Views/Chat/ChatView.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

68 lines
2.1 KiB
Swift

import SwiftData
import SwiftUI
struct ChatView: View {
@Bindable var conversation: Conversation
var orchestrator: ChatOrchestrator
@Environment(\.modelContext) private var modelContext
@State private var inputText: String = ""
var body: some View {
VStack(spacing: 0) {
// Messages
MessageList(
conversation: conversation,
streamingContent: orchestrator.streamingContent,
isStreaming: orchestrator.isStreaming,
liveToolCalls: orchestrator.liveToolCalls
)
// Error banner
if let error = orchestrator.error {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
Text(error)
.font(.callout)
.foregroundStyle(.red)
Spacer()
Button("Dismiss") {
orchestrator.error = nil
}
.buttonStyle(.borderless)
.font(.callout)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.red.opacity(0.1))
}
// Input bar
ChatInputBar(
text: $inputText,
isStreaming: orchestrator.isStreaming,
onSend: sendMessage
)
}
.navigationTitle(conversation.displayTitle)
.toolbar {
ToolbarItem(placement: .automatic) {
if orchestrator.isStreaming {
ProgressView()
.controlSize(.small)
}
}
}
}
private func sendMessage() {
let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
inputText = ""
Task {
await orchestrator.sendMessage(text, conversation: conversation, modelContext: modelContext)
}
}
}