6218a6ef28
- NSTextView-based syntax highlighting with regex tokenizer - Swift, C/C++, ObjC, JSON keywords, types, strings, comments - Theme-aware coloring, debounced re-highlighting - Workspace-wide search across all source files - Grouped results by file with line numbers - Scope toggle: current file vs all files - Git status badges on file tree nodes - GitService changes flow to FileNode.gitStatus - Agent file operations refresh file tree - Auto-restore last workspace on launch - All tests passing (0 errors, 0 warnings)
785 lines
26 KiB
Swift
785 lines
26 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - VSCode Dark+ Color Constants
|
|
|
|
enum VSC {
|
|
static let editorBg = Color(red: 0.118, green: 0.118, blue: 0.118)
|
|
static let sidebarBg = Color(red: 0.145, green: 0.145, blue: 0.149)
|
|
static let activityBarBg = Color(red: 0.20, green: 0.20, blue: 0.20)
|
|
static let tabBarBg = Color(red: 0.176, green: 0.176, blue: 0.176)
|
|
static let activeTabBg = Color(red: 0.118, green: 0.118, blue: 0.118)
|
|
static let statusBarBg = Color(red: 0.0, green: 0.478, blue: 0.8)
|
|
static let statusBarNoFolder = Color(red: 0.404, green: 0.227, blue: 0.718)
|
|
static let panelBg = Color(red: 0.118, green: 0.118, blue: 0.118)
|
|
static let titleBarBg = Color(red: 0.188, green: 0.188, blue: 0.192)
|
|
static let border = Color(red: 0.235, green: 0.235, blue: 0.235)
|
|
static let text = Color(white: 0.85)
|
|
static let dimText = Color(white: 0.55)
|
|
static let accentBlue = Color(red: 0.0, green: 0.478, blue: 0.8)
|
|
static let focusBorder = Color(red: 0.0, green: 0.478, blue: 0.8)
|
|
static let activityActive = Color.white
|
|
static let activityInactive = Color(white: 0.5)
|
|
static let badgeBg = Color(red: 0.0, green: 0.478, blue: 0.8)
|
|
static let inputBg = Color(red: 0.188, green: 0.188, blue: 0.188)
|
|
static let inputBorder = Color(red: 0.27, green: 0.27, blue: 0.27)
|
|
static let listHover = Color(white: 0.18)
|
|
static let listActive = Color(red: 0.02, green: 0.35, blue: 0.55)
|
|
static let errorFg = Color(red: 0.94, green: 0.31, blue: 0.27)
|
|
static let warningFg = Color(red: 0.80, green: 0.69, blue: 0.26)
|
|
static let breadcrumbFg = Color(white: 0.6)
|
|
static let gutterBg = Color(red: 0.118, green: 0.118, blue: 0.118)
|
|
static let gutterFg = Color(white: 0.35)
|
|
static let lineHighlight = Color(white: 0.165)
|
|
static let selection = Color(red: 0.149, green: 0.310, blue: 0.471)
|
|
static let minimapBg = Color(red: 0.118, green: 0.118, blue: 0.118)
|
|
static let scrollbar = Color(white: 0.4).opacity(0.4)
|
|
}
|
|
|
|
// MARK: - Error Types
|
|
|
|
enum IDEErrorCode: Error, LocalizedError {
|
|
case fileNotFound
|
|
case compilerError(String)
|
|
case runtimePanic
|
|
case engineUnavailable
|
|
case fileAccessDenied(String)
|
|
case invalidEncoding
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .fileNotFound: return "File not found."
|
|
case .compilerError(let msg): return "Compiler error: \(msg)"
|
|
case .runtimePanic: return "Runtime panic occurred."
|
|
case .engineUnavailable: return "Code engine is unavailable."
|
|
case .fileAccessDenied(let path): return "Access denied: \(path)"
|
|
case .invalidEncoding: return "Invalid file encoding."
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Diagnostics
|
|
|
|
struct Diagnostic: Identifiable {
|
|
let id = UUID()
|
|
let line: Int
|
|
let column: Int
|
|
let message: String
|
|
let severity: DiagnosticSeverity
|
|
|
|
enum DiagnosticSeverity: String, CaseIterable {
|
|
case error, warning, info
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .error: return "xmark.circle.fill"
|
|
case .warning: return "exclamationmark.triangle.fill"
|
|
case .info: return "info.circle.fill"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .error: return .red
|
|
case .warning: return .yellow
|
|
case .info: return .blue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Console
|
|
|
|
struct ConsoleEntry: Identifiable {
|
|
let id = UUID()
|
|
let timestamp: Date
|
|
let message: String
|
|
let level: Level
|
|
|
|
enum Level: String, CaseIterable {
|
|
case output, error, system, debug, stdin, agent
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .output: return .green
|
|
case .error: return .red
|
|
case .system: return .cyan
|
|
case .debug: return .gray
|
|
case .stdin: return .orange
|
|
case .agent: return .purple
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .output: return "arrow.right"
|
|
case .error: return "xmark.circle"
|
|
case .system: return "gearshape"
|
|
case .debug: return "ant"
|
|
case .stdin: return "keyboard"
|
|
case .agent: return "bubble.left"
|
|
}
|
|
}
|
|
}
|
|
|
|
var formattedTimestamp: String {
|
|
Self.timestampFormatter.string(from: timestamp)
|
|
}
|
|
|
|
private static let timestampFormatter: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "HH:mm:ss.SSS"
|
|
return f
|
|
}()
|
|
}
|
|
|
|
// MARK: - Editor Tabs
|
|
|
|
struct EditorTab: Identifiable, Hashable {
|
|
let id: UUID
|
|
let name: String
|
|
let url: URL?
|
|
var content: String
|
|
var isModified: Bool
|
|
var language: Language
|
|
var cursorLine: Int
|
|
var cursorColumn: Int
|
|
|
|
init(name: String, url: URL? = nil, content: String = "", isModified: Bool = false,
|
|
language: Language = .swift, cursorLine: Int = 1, cursorColumn: Int = 1) {
|
|
self.id = UUID()
|
|
self.name = name
|
|
self.url = url
|
|
self.content = content
|
|
self.isModified = isModified
|
|
self.language = language
|
|
self.cursorLine = cursorLine
|
|
self.cursorColumn = cursorColumn
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) { hasher.combine(id) }
|
|
static func == (lhs: EditorTab, rhs: EditorTab) -> Bool { lhs.id == rhs.id }
|
|
|
|
enum Language: String, CaseIterable {
|
|
case swift, objc, cpp, c, header, json, markdown, plaintext
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .swift: return "Swift"
|
|
case .objc: return "Objective-C"
|
|
case .cpp: return "C++"
|
|
case .c: return "C"
|
|
case .header: return "Header"
|
|
case .json: return "JSON"
|
|
case .markdown: return "Markdown"
|
|
case .plaintext: return "Plain Text"
|
|
}
|
|
}
|
|
|
|
var iconColor: Color {
|
|
switch self {
|
|
case .swift: return .orange
|
|
case .objc: return .teal
|
|
case .cpp: return .cyan
|
|
case .c: return .blue
|
|
case .header: return .purple
|
|
case .json: return .yellow
|
|
case .markdown: return .green
|
|
case .plaintext: return .gray
|
|
}
|
|
}
|
|
|
|
static func from(extension ext: String) -> Language {
|
|
switch ext.lowercased() {
|
|
case "swift": return .swift
|
|
case "m", "mm": return .objc
|
|
case "cpp", "cc", "cxx": return .cpp
|
|
case "c": return .c
|
|
case "h", "hpp": return .header
|
|
case "json": return .json
|
|
case "md", "markdown": return .markdown
|
|
default: return .plaintext
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Search & Replace
|
|
|
|
struct SearchState {
|
|
var query: String = ""
|
|
var replacement: String = ""
|
|
var isActive: Bool = false
|
|
var isCaseSensitive: Bool = false
|
|
var isRegex: Bool = false
|
|
var isWholeWord: Bool = false
|
|
var matchCount: Int = 0
|
|
var currentMatchIndex: Int = 0
|
|
var scope: SearchScope = .currentFile
|
|
|
|
enum SearchScope: String, CaseIterable {
|
|
case currentFile = "Current File"
|
|
case allFiles = "All Files"
|
|
}
|
|
|
|
var hasMatches: Bool { matchCount > 0 }
|
|
}
|
|
|
|
// MARK: - Workspace Search Result
|
|
|
|
struct WorkspaceSearchResult: Identifiable {
|
|
let id = UUID()
|
|
let fileURL: URL
|
|
let fileName: String
|
|
let line: Int
|
|
let column: Int
|
|
let lineContent: String
|
|
let matchRange: Range<String.Index>
|
|
}
|
|
|
|
// MARK: - Theme
|
|
|
|
struct IDETheme: Identifiable, Hashable {
|
|
let id: String
|
|
let name: String
|
|
let editorBackground: Color
|
|
let editorForeground: Color
|
|
let gutterBackground: Color
|
|
let gutterForeground: Color
|
|
let sidebarBackground: Color
|
|
let toolbarBackground: Color
|
|
let statusBarBackground: Color
|
|
let selectionColor: Color
|
|
let currentLineColor: Color
|
|
let keywordColor: Color
|
|
let stringColor: Color
|
|
let commentColor: Color
|
|
let numberColor: Color
|
|
let typeColor: Color
|
|
let functionColor: Color
|
|
let preprocessorColor: Color
|
|
|
|
func hash(into hasher: inout Hasher) { hasher.combine(id) }
|
|
static func == (lhs: IDETheme, rhs: IDETheme) -> Bool { lhs.id == rhs.id }
|
|
|
|
static let midnight = IDETheme(
|
|
id: "midnight", name: "Midnight",
|
|
editorBackground: Color(red: 0.06, green: 0.06, blue: 0.10),
|
|
editorForeground: Color(white: 0.92),
|
|
gutterBackground: Color(red: 0.05, green: 0.05, blue: 0.08),
|
|
gutterForeground: Color(white: 0.35),
|
|
sidebarBackground: Color(red: 0.07, green: 0.07, blue: 0.09),
|
|
toolbarBackground: Color(red: 0.08, green: 0.08, blue: 0.10),
|
|
statusBarBackground: Color(red: 0.06, green: 0.06, blue: 0.08),
|
|
selectionColor: Color.blue.opacity(0.3),
|
|
currentLineColor: Color(white: 0.12),
|
|
keywordColor: Color(red: 0.85, green: 0.35, blue: 0.65),
|
|
stringColor: Color(red: 0.90, green: 0.40, blue: 0.30),
|
|
commentColor: Color(red: 0.45, green: 0.55, blue: 0.40),
|
|
numberColor: Color(red: 0.85, green: 0.75, blue: 0.40),
|
|
typeColor: Color(red: 0.55, green: 0.75, blue: 0.85),
|
|
functionColor: Color(red: 0.40, green: 0.70, blue: 0.90),
|
|
preprocessorColor: Color(red: 0.70, green: 0.50, blue: 0.80)
|
|
)
|
|
|
|
static let xcodeDark = IDETheme(
|
|
id: "xcode_dark", name: "Xcode Dark",
|
|
editorBackground: Color(red: 0.11, green: 0.12, blue: 0.14),
|
|
editorForeground: Color(white: 0.90),
|
|
gutterBackground: Color(red: 0.10, green: 0.10, blue: 0.12),
|
|
gutterForeground: Color(white: 0.40),
|
|
sidebarBackground: Color(red: 0.12, green: 0.12, blue: 0.14),
|
|
toolbarBackground: Color(red: 0.14, green: 0.14, blue: 0.16),
|
|
statusBarBackground: Color(red: 0.10, green: 0.10, blue: 0.12),
|
|
selectionColor: Color.blue.opacity(0.25),
|
|
currentLineColor: Color(white: 0.16),
|
|
keywordColor: Color(red: 0.99, green: 0.37, blue: 0.53),
|
|
stringColor: Color(red: 0.99, green: 0.42, blue: 0.33),
|
|
commentColor: Color(red: 0.42, green: 0.56, blue: 0.37),
|
|
numberColor: Color(red: 0.85, green: 0.75, blue: 0.45),
|
|
typeColor: Color(red: 0.36, green: 0.85, blue: 0.76),
|
|
functionColor: Color(red: 0.40, green: 0.72, blue: 0.92),
|
|
preprocessorColor: Color(red: 0.68, green: 0.51, blue: 0.87)
|
|
)
|
|
|
|
static let solarized = IDETheme(
|
|
id: "solarized", name: "Solarized Dark",
|
|
editorBackground: Color(red: 0.00, green: 0.17, blue: 0.21),
|
|
editorForeground: Color(red: 0.51, green: 0.58, blue: 0.59),
|
|
gutterBackground: Color(red: 0.00, green: 0.15, blue: 0.19),
|
|
gutterForeground: Color(red: 0.35, green: 0.43, blue: 0.46),
|
|
sidebarBackground: Color(red: 0.00, green: 0.14, blue: 0.18),
|
|
toolbarBackground: Color(red: 0.02, green: 0.19, blue: 0.23),
|
|
statusBarBackground: Color(red: 0.00, green: 0.13, blue: 0.17),
|
|
selectionColor: Color(red: 0.07, green: 0.26, blue: 0.30),
|
|
currentLineColor: Color(red: 0.02, green: 0.21, blue: 0.25),
|
|
keywordColor: Color(red: 0.52, green: 0.60, blue: 0.00),
|
|
stringColor: Color(red: 0.16, green: 0.63, blue: 0.60),
|
|
commentColor: Color(red: 0.35, green: 0.43, blue: 0.46),
|
|
numberColor: Color(red: 0.80, green: 0.29, blue: 0.09),
|
|
typeColor: Color(red: 0.15, green: 0.55, blue: 0.82),
|
|
functionColor: Color(red: 0.42, green: 0.44, blue: 0.77),
|
|
preprocessorColor: Color(red: 0.83, green: 0.21, blue: 0.51)
|
|
)
|
|
|
|
static let allThemes: [IDETheme] = [.vscodeDark, .midnight, .xcodeDark, .solarized]
|
|
|
|
static let vscodeDark = IDETheme(
|
|
id: "vscode_dark", name: "Dark+ (VSCode)",
|
|
editorBackground: Color(red: 0.118, green: 0.118, blue: 0.118),
|
|
editorForeground: Color(red: 0.82, green: 0.82, blue: 0.82),
|
|
gutterBackground: Color(red: 0.118, green: 0.118, blue: 0.118),
|
|
gutterForeground: Color(white: 0.35),
|
|
sidebarBackground: Color(red: 0.145, green: 0.145, blue: 0.149),
|
|
toolbarBackground: Color(red: 0.176, green: 0.176, blue: 0.176),
|
|
statusBarBackground: Color(red: 0.0, green: 0.478, blue: 0.8),
|
|
selectionColor: Color(red: 0.149, green: 0.310, blue: 0.471),
|
|
currentLineColor: Color(red: 0.165, green: 0.176, blue: 0.180),
|
|
keywordColor: Color(red: 0.337, green: 0.612, blue: 0.839),
|
|
stringColor: Color(red: 0.808, green: 0.569, blue: 0.471),
|
|
commentColor: Color(red: 0.416, green: 0.600, blue: 0.333),
|
|
numberColor: Color(red: 0.710, green: 0.808, blue: 0.659),
|
|
typeColor: Color(red: 0.306, green: 0.788, blue: 0.690),
|
|
functionColor: Color(red: 0.863, green: 0.863, blue: 0.667),
|
|
preprocessorColor: Color(red: 0.773, green: 0.525, blue: 0.753)
|
|
)
|
|
}
|
|
|
|
// MARK: - Build Configuration
|
|
|
|
struct BuildConfiguration: Identifiable, Hashable {
|
|
let id: String
|
|
let name: String
|
|
var arguments: [String]
|
|
var environmentVariables: [String: String]
|
|
var workingDirectory: URL?
|
|
var optimizationLevel: OptimizationLevel
|
|
|
|
enum OptimizationLevel: String, CaseIterable {
|
|
case debug = "-Onone"
|
|
case release = "-O"
|
|
case unchecked = "-Ounchecked"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .debug: return "Debug (-Onone)"
|
|
case .release: return "Release (-O)"
|
|
case .unchecked: return "Unchecked (-Ounchecked)"
|
|
}
|
|
}
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) { hasher.combine(id) }
|
|
static func == (lhs: BuildConfiguration, rhs: BuildConfiguration) -> Bool { lhs.id == rhs.id }
|
|
|
|
static let `default` = BuildConfiguration(
|
|
id: "default", name: "Debug",
|
|
arguments: [], environmentVariables: [:],
|
|
workingDirectory: nil, optimizationLevel: .debug
|
|
)
|
|
}
|
|
|
|
// MARK: - Snippets
|
|
|
|
struct CodeSnippet: Identifiable, Hashable {
|
|
let id: String
|
|
let title: String
|
|
let code: String
|
|
let language: EditorTab.Language
|
|
let category: String
|
|
|
|
static let builtIn: [CodeSnippet] = [
|
|
CodeSnippet(id: "hello", title: "Hello World", code: "print(\"Hello, World!\")", language: .swift, category: "Basics"),
|
|
CodeSnippet(id: "func", title: "Function", code: "func name(param: Type) -> ReturnType {\n \n}", language: .swift, category: "Basics"),
|
|
CodeSnippet(id: "struct", title: "Struct", code: "struct Name {\n let property: Type\n}", language: .swift, category: "Types"),
|
|
CodeSnippet(id: "class", title: "Class", code: "class Name {\n init() {\n \n }\n}", language: .swift, category: "Types"),
|
|
CodeSnippet(id: "enum", title: "Enum", code: "enum Name {\n case first\n case second\n}", language: .swift, category: "Types"),
|
|
CodeSnippet(id: "protocol", title: "Protocol", code: "protocol Name {\n func method()\n}", language: .swift, category: "Types"),
|
|
CodeSnippet(id: "guard", title: "Guard Let", code: "guard let value = optional else {\n return\n}", language: .swift, category: "Control Flow"),
|
|
CodeSnippet(id: "iflet", title: "If Let", code: "if let value = optional {\n \n}", language: .swift, category: "Control Flow"),
|
|
CodeSnippet(id: "forin", title: "For-In Loop", code: "for item in collection {\n \n}", language: .swift, category: "Control Flow"),
|
|
CodeSnippet(id: "switch", title: "Switch", code: "switch value {\ncase .first:\n break\ncase .second:\n break\n}", language: .swift, category: "Control Flow"),
|
|
CodeSnippet(id: "docatch", title: "Do-Catch", code: "do {\n try expression\n} catch {\n print(error)\n}", language: .swift, category: "Error Handling"),
|
|
CodeSnippet(id: "async_func", title: "Async Function", code: "func fetch() async throws -> Data {\n \n}", language: .swift, category: "Concurrency"),
|
|
CodeSnippet(id: "task", title: "Task", code: "Task {\n await doWork()\n}", language: .swift, category: "Concurrency"),
|
|
CodeSnippet(id: "closure", title: "Closure", code: "{ [weak self] param in\n guard let self else { return }\n \n}", language: .swift, category: "Closures"),
|
|
CodeSnippet(id: "ext", title: "Extension", code: "extension TypeName {\n \n}", language: .swift, category: "Types"),
|
|
]
|
|
}
|
|
|
|
// MARK: - Breakpoint
|
|
|
|
struct Breakpoint: Identifiable, Hashable {
|
|
let id = UUID()
|
|
let file: String
|
|
var line: Int
|
|
var isEnabled: Bool = true
|
|
var condition: String? = nil
|
|
|
|
func hash(into hasher: inout Hasher) { hasher.combine(id) }
|
|
static func == (lhs: Breakpoint, rhs: Breakpoint) -> Bool { lhs.id == rhs.id }
|
|
}
|
|
|
|
// MARK: - Execution History
|
|
|
|
struct ExecutionRecord: Identifiable {
|
|
let id = UUID()
|
|
let timestamp: Date
|
|
let source: String
|
|
let output: String
|
|
let exitCode: Int32
|
|
let duration: TimeInterval
|
|
|
|
var succeeded: Bool { exitCode == 0 }
|
|
|
|
var formattedTimestamp: String {
|
|
Self.formatter.string(from: timestamp)
|
|
}
|
|
|
|
var formattedDuration: String {
|
|
String(format: "%.2fs", duration)
|
|
}
|
|
|
|
private static let formatter: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "MMM d, HH:mm:ss"
|
|
return f
|
|
}()
|
|
}
|
|
|
|
// MARK: - Activity Panel
|
|
|
|
enum ActivityPanel: String, CaseIterable {
|
|
case explorer = "Explorer"
|
|
case search = "Search"
|
|
case git = "Source Control"
|
|
case snippets = "Snippets"
|
|
case debug = "Debug"
|
|
case agent = "Agent"
|
|
case chat = "Chat"
|
|
case settings = "Settings"
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .explorer: return "doc.on.doc"
|
|
case .search: return "magnifyingglass"
|
|
case .git: return "point.3.filled.connected.trianglepath.dotted"
|
|
case .snippets: return "text.insert"
|
|
case .debug: return "ant"
|
|
case .agent: return "bubble.left.and.text.bubble.right"
|
|
case .chat: return "message"
|
|
case .settings: return "gearshape"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Command Palette
|
|
|
|
struct CommandItem: Identifiable {
|
|
let id = UUID()
|
|
let title: String
|
|
let subtitle: String?
|
|
let shortcut: String?
|
|
let icon: String
|
|
let action: () -> Void
|
|
|
|
init(title: String, subtitle: String? = nil, shortcut: String? = nil, icon: String = "command", action: @escaping () -> Void) {
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
self.shortcut = shortcut
|
|
self.icon = icon
|
|
self.action = action
|
|
}
|
|
}
|
|
|
|
// MARK: - Agent Types
|
|
|
|
struct AgentMessage: Identifiable {
|
|
let id = UUID()
|
|
let role: Role
|
|
let content: String
|
|
let timestamp: Date
|
|
var toolName: String?
|
|
var isStreaming: Bool
|
|
var isError: Bool
|
|
|
|
enum Role: String {
|
|
case user, agent, system, tool
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .user: return .blue
|
|
case .agent: return .purple
|
|
case .system: return .cyan
|
|
case .tool: return .orange
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .user: return "person.fill"
|
|
case .agent: return "cpu"
|
|
case .system: return "gearshape.fill"
|
|
case .tool: return "wrench.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
init(role: Role, content: String, toolName: String? = nil,
|
|
isStreaming: Bool = false, isError: Bool = false) {
|
|
self.role = role
|
|
self.content = content
|
|
self.timestamp = Date()
|
|
self.toolName = toolName
|
|
self.isStreaming = isStreaming
|
|
self.isError = isError
|
|
}
|
|
|
|
var formattedTimestamp: String {
|
|
Self.formatter.string(from: timestamp)
|
|
}
|
|
|
|
private static let formatter: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "HH:mm:ss"
|
|
return f
|
|
}()
|
|
}
|
|
|
|
enum AgentStatus: Equatable {
|
|
case idle
|
|
case thinking
|
|
case executing(String)
|
|
case error(String)
|
|
|
|
var displayText: String {
|
|
switch self {
|
|
case .idle: return "Ready"
|
|
case .thinking: return "Thinking..."
|
|
case .executing(let tool): return "Running: \(tool)"
|
|
case .error(let msg): return "Error: \(msg)"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .idle: return .green
|
|
case .thinking: return .yellow
|
|
case .executing: return .orange
|
|
case .error: return .red
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .idle: return "checkmark.circle.fill"
|
|
case .thinking: return "brain"
|
|
case .executing: return "gearshape.2.fill"
|
|
case .error: return "exclamationmark.triangle.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AgentToolInfo: Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let description: String
|
|
let category: String
|
|
let isReadOnly: Bool
|
|
|
|
init(name: String, description: String, category: String = "General", isReadOnly: Bool = false) {
|
|
self.id = name
|
|
self.name = name
|
|
self.description = description
|
|
self.category = category
|
|
self.isReadOnly = isReadOnly
|
|
}
|
|
}
|
|
|
|
// MARK: - AI Model Configuration
|
|
|
|
enum AIProvider: String, CaseIterable, Identifiable, Codable {
|
|
case local = "NVIDIA"
|
|
case openAI = "OpenAI"
|
|
case anthropic = "Anthropic"
|
|
case ollama = "Ollama"
|
|
|
|
var id: String { rawValue }
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .local: return "cpu"
|
|
case .openAI: return "globe"
|
|
case .anthropic: return "brain.head.profile"
|
|
case .ollama: return "server.rack"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .local: return .green
|
|
case .openAI: return .cyan
|
|
case .anthropic: return .orange
|
|
case .ollama: return .purple
|
|
}
|
|
}
|
|
|
|
var models: [AIModel] {
|
|
switch self {
|
|
case .local:
|
|
return [
|
|
AIModel(id: "meta/llama-3.3-70b-instruct", name: "Llama 3.3 70B", provider: .local, contextWindow: 128_000, isLocal: false),
|
|
AIModel(id: "meta/llama-3.1-405b-instruct", name: "Llama 3.1 405B", provider: .local, contextWindow: 128_000, isLocal: false),
|
|
AIModel(id: "qwen/qwen2.5-coder-32b-instruct", name: "Qwen 2.5 Coder 32B", provider: .local, contextWindow: 32_000, isLocal: false),
|
|
]
|
|
case .openAI:
|
|
return [
|
|
AIModel(id: "gpt-4o", name: "GPT-4o", provider: .openAI, contextWindow: 128_000, isLocal: false),
|
|
AIModel(id: "gpt-4o-mini", name: "GPT-4o Mini", provider: .openAI, contextWindow: 128_000, isLocal: false),
|
|
AIModel(id: "o3-mini", name: "o3-mini", provider: .openAI, contextWindow: 128_000, isLocal: false),
|
|
]
|
|
case .anthropic:
|
|
return [
|
|
AIModel(id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: .anthropic, contextWindow: 200_000, isLocal: false),
|
|
AIModel(id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku", provider: .anthropic, contextWindow: 200_000, isLocal: false),
|
|
]
|
|
case .ollama:
|
|
return [
|
|
AIModel(id: "codellama:13b", name: "CodeLlama 13B", provider: .ollama, contextWindow: 16_000, isLocal: true),
|
|
AIModel(id: "deepseek-coder:6.7b", name: "DeepSeek Coder", provider: .ollama, contextWindow: 16_000, isLocal: true),
|
|
AIModel(id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder", provider: .ollama, contextWindow: 32_000, isLocal: true),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// The base API endpoint for this provider
|
|
var endpoint: String {
|
|
switch self {
|
|
case .local: return "https://integrate.api.nvidia.com/v1/chat/completions"
|
|
case .openAI: return "https://api.openai.com/v1/chat/completions"
|
|
case .anthropic: return "https://api.anthropic.com/v1/messages"
|
|
case .ollama: return "http://127.0.0.1:11434/v1/chat/completions"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AIModel: Identifiable, Hashable, Codable {
|
|
let id: String
|
|
let name: String
|
|
let provider: AIProvider
|
|
let contextWindow: Int
|
|
let isLocal: Bool
|
|
|
|
var displayName: String {
|
|
"\(name) (\(provider.rawValue))"
|
|
}
|
|
|
|
var contextLabel: String {
|
|
let k = contextWindow / 1000
|
|
return "\(k)K"
|
|
}
|
|
}
|
|
|
|
struct ChatMessage: Identifiable {
|
|
let id: UUID
|
|
let role: ChatRole
|
|
var content: String
|
|
let timestamp: Date
|
|
var toolResults: [ToolResult]
|
|
var isStreaming: Bool
|
|
var isError: Bool
|
|
var autoApplied: Bool
|
|
|
|
enum ChatRole: String {
|
|
case user, assistant, system, tool
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .user: return .blue
|
|
case .assistant: return .purple
|
|
case .system: return .cyan
|
|
case .tool: return .orange
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .user: return "person.fill"
|
|
case .assistant: return "cpu"
|
|
case .system: return "gearshape.fill"
|
|
case .tool: return "wrench.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ToolResult: Identifiable {
|
|
let id = UUID()
|
|
let toolName: String
|
|
let output: String
|
|
let isError: Bool
|
|
}
|
|
|
|
init(role: ChatRole, content: String, toolResults: [ToolResult] = [],
|
|
isStreaming: Bool = false, isError: Bool = false, autoApplied: Bool = false) {
|
|
self.id = UUID()
|
|
self.role = role
|
|
self.content = content
|
|
self.timestamp = Date()
|
|
self.toolResults = toolResults
|
|
self.isStreaming = isStreaming
|
|
self.isError = isError
|
|
self.autoApplied = autoApplied
|
|
}
|
|
|
|
var formattedTime: String {
|
|
Self.formatter.string(from: timestamp)
|
|
}
|
|
|
|
private static let formatter: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "HH:mm"
|
|
return f
|
|
}()
|
|
}
|
|
|
|
struct ChatSession: Identifiable {
|
|
let id = UUID()
|
|
var title: String
|
|
var messages: [ChatMessage]
|
|
var model: AIModel
|
|
let created: Date
|
|
|
|
init(title: String = "New Chat", model: AIModel) {
|
|
self.title = title
|
|
self.messages = []
|
|
self.model = model
|
|
self.created = Date()
|
|
}
|
|
|
|
var preview: String {
|
|
messages.first(where: { $0.role == .user })?.content.prefix(60).description ?? "Empty chat"
|
|
}
|
|
}
|
|
|
|
enum ChatMode: String, CaseIterable {
|
|
case chat = "Chat"
|
|
case agent = "Agent"
|
|
case edit = "Edit"
|
|
|
|
var iconName: String {
|
|
switch self {
|
|
case .chat: return "bubble.left.and.bubble.right"
|
|
case .agent: return "cpu"
|
|
case .edit: return "pencil.line"
|
|
}
|
|
}
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .chat: return "Ask questions about your code"
|
|
case .agent: return "Agent can create, edit, run & build"
|
|
case .edit: return "Direct inline editing"
|
|
}
|
|
}
|
|
}
|