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>
This commit is contained in:
Thomas Davis 2026-02-09 21:29:39 +10:00
parent c8e8a7d9b4
commit 1cd44b4e97
45 changed files with 4583 additions and 0 deletions

View file

@ -0,0 +1,41 @@
import Foundation
import SwiftData
@Model
final class Conversation {
var id: UUID
var title: String?
var createdAt: Date
var updatedAt: Date
var executionState: String // "idle" | "running"
var inputTokensTotal: Int
var outputTokensTotal: Int
@Relationship(deleteRule: .cascade, inverse: \Message.conversation)
var messages: [Message]
@Relationship(deleteRule: .cascade, inverse: \ToolCallRecord.conversation)
var toolRuns: [ToolCallRecord]
init(
title: String? = nil
) {
self.id = UUID()
self.title = title
self.createdAt = Date()
self.updatedAt = Date()
self.executionState = "idle"
self.inputTokensTotal = 0
self.outputTokensTotal = 0
self.messages = []
self.toolRuns = []
}
var displayTitle: String {
title ?? "New Conversation"
}
var sortedMessages: [Message] {
messages.sorted { $0.createdAt < $1.createdAt }
}
}

View file

@ -0,0 +1,18 @@
import Foundation
import SwiftData
@Model
final class EnvVar {
var id: UUID
var keyName: String
/// Last 4 characters of the value (for display hint)
var valueHint: String
var createdAt: Date
init(keyName: String, valueHint: String) {
self.id = UUID()
self.keyName = keyName
self.valueHint = valueHint
self.createdAt = Date()
}
}

View file

@ -0,0 +1,141 @@
import Foundation
import SwiftData
enum MessageRole: String, Codable {
case user = "USER"
case assistant = "ASSISTANT"
case tool = "TOOL"
case system = "SYSTEM"
}
@Model
final class Message {
var id: UUID
var role: MessageRole
var content: String
var createdAt: Date
var inputTokens: Int?
var outputTokens: Int?
/// JSON-encoded array of tool calls (for assistant messages)
var toolCallsJSON: Data?
var conversation: Conversation?
init(
role: MessageRole,
content: String,
conversation: Conversation? = nil,
inputTokens: Int? = nil,
outputTokens: Int? = nil,
toolCalls: [ToolCallData]? = nil
) {
self.id = UUID()
self.role = role
self.content = content
self.createdAt = Date()
self.inputTokens = inputTokens
self.outputTokens = outputTokens
self.conversation = conversation
if let toolCalls {
self.toolCallsJSON = try? JSONEncoder().encode(toolCalls)
}
}
var toolCalls: [ToolCallData] {
get {
guard let data = toolCallsJSON else { return [] }
return (try? JSONDecoder().decode([ToolCallData].self, from: data)) ?? []
}
set {
toolCallsJSON = try? JSONEncoder().encode(newValue)
}
}
}
/// Serializable tool call data stored in messages
struct ToolCallData: Codable, Identifiable {
var id: String { toolCallId }
let toolCallId: String
let toolName: String
let args: JSONValue?
let output: JSONValue?
init(toolCallId: String, toolName: String, args: JSONValue? = nil, output: JSONValue? = nil) {
self.toolCallId = toolCallId
self.toolName = toolName
self.args = args
self.output = output
}
}
/// A type-erased JSON value for encoding/decoding arbitrary JSON
enum JSONValue: Codable, Equatable, Sendable {
case string(String)
case number(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
case null
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let b = try? container.decode(Bool.self) {
self = .bool(b)
} else if let n = try? container.decode(Double.self) {
self = .number(n)
} else if let s = try? container.decode(String.self) {
self = .string(s)
} else if let arr = try? container.decode([JSONValue].self) {
self = .array(arr)
} else if let obj = try? container.decode([String: JSONValue].self) {
self = .object(obj)
} else {
self = .null
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let s): try container.encode(s)
case .number(let n): try container.encode(n)
case .bool(let b): try container.encode(b)
case .object(let o): try container.encode(o)
case .array(let a): try container.encode(a)
case .null: try container.encodeNil()
}
}
/// Convert any Codable/Sendable value to JSONValue
static func from(_ value: Any) -> JSONValue {
if let s = value as? String { return .string(s) }
if let n = value as? NSNumber {
if CFBooleanGetTypeID() == CFGetTypeID(n) {
return .bool(n.boolValue)
}
return .number(n.doubleValue)
}
if let b = value as? Bool { return .bool(b) }
if let i = value as? Int { return .number(Double(i)) }
if let d = value as? Double { return .number(d) }
if let arr = value as? [Any] { return .array(arr.map { from($0) }) }
if let obj = value as? [String: Any] {
return .object(obj.mapValues { from($0) })
}
return .null
}
/// Pretty-print JSON
var prettyString: String {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let data = try? encoder.encode(self),
let str = String(data: data, encoding: .utf8) else {
return "null"
}
return str
}
}

View file

@ -0,0 +1,51 @@
import Foundation
import SwiftData
@Model
final class ToolCallRecord {
var id: UUID
var toolName: String
var toolCallId: String
var status: String // "running" | "success" | "error"
var inputJSON: Data?
var outputJSON: Data?
var errorMessage: String?
var executionTimeMs: Int?
var createdAt: Date
var completedAt: Date?
var conversation: Conversation?
init(
toolName: String,
toolCallId: String,
conversation: Conversation? = nil
) {
self.id = UUID()
self.toolName = toolName
self.toolCallId = toolCallId
self.status = "running"
self.createdAt = Date()
self.conversation = conversation
}
var input: JSONValue? {
get {
guard let data = inputJSON else { return nil }
return try? JSONDecoder().decode(JSONValue.self, from: data)
}
set {
inputJSON = try? JSONEncoder().encode(newValue)
}
}
var output: JSONValue? {
get {
guard let data = outputJSON else { return nil }
return try? JSONDecoder().decode(JSONValue.self, from: data)
}
set {
outputJSON = try? JSONEncoder().encode(newValue)
}
}
}

View file

@ -0,0 +1,17 @@
import Foundation
import SwiftData
@Model
final class UserSettings {
var id: UUID
var systemPrompt: String?
var selectedModel: String
var pinnedToolIds: [String]
init() {
self.id = UUID()
self.systemPrompt = nil
self.selectedModel = "gpt-4.1-mini"
self.pinnedToolIds = []
}
}