Files
CxIDE/Models/CodeEngineProtocol.swift
T
cx-git-agent c118996746 feat: CxIDE v1 — native macOS SwiftUI IDE with agentic AI assistant
- SwiftUI macOS app with C++17 code analysis engine (ObjC++ bridge)
- Agentic AI loop: LLM plans → tool calls → execution → feedback loop
- 15 agent tools: file ops, terminal, git, xcode build, code intel
- 7 persistent terminal tools with background session management
- Chat sidebar with agent step rendering and auto-apply
- NVIDIA NIM API integration (Llama 3.3 70B default)
- OpenAI tool_calls format with prompt-based fallback
- Code editor with syntax highlighting and multi-tab support
- File tree, console view, terminal view
- Git integration and workspace management
2026-04-21 16:05:52 -05:00

164 lines
5.7 KiB
Swift

import Foundation
protocol CodeEngineProtocol: AnyObject {
func execute(script: String) async throws -> String
func analyze(script: String) async throws -> [Diagnostic]
func cancel()
}
extension CodeEngineProtocol {
func cancel() {}
}
final class ProjectCodeEngine: CodeEngineProtocol {
private let workingDirectory: URL
private let timeoutSeconds: TimeInterval
private var currentProcess: Process?
init(workingDirectory: URL = FileManager.default.temporaryDirectory, timeoutSeconds: TimeInterval = 30) {
self.workingDirectory = workingDirectory
self.timeoutSeconds = timeoutSeconds
}
func execute(script: String) async throws -> String {
let tempDir = workingDirectory
let fileURL = tempDir.appendingPathComponent("exec_\(UUID().uuidString).swift")
try script.write(to: fileURL, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: fileURL) }
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/swift")
process.arguments = [fileURL.path]
process.currentDirectoryURL = tempDir
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
currentProcess = process
do {
try process.run()
} catch {
currentProcess = nil
throw IDEErrorCode.compilerError(error.localizedDescription)
}
let timer = DispatchSource.makeTimerSource(queue: .global())
timer.schedule(deadline: .now() + timeoutSeconds)
timer.setEventHandler { [weak process] in
process?.terminate()
}
timer.resume()
let outData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
let errData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
timer.cancel()
currentProcess = nil
let stdout = String(data: outData, encoding: .utf8) ?? ""
let stderr = String(data: errData, encoding: .utf8) ?? ""
if process.terminationStatus != 0 {
let detail = stderr.isEmpty ? "Exit code: \(process.terminationStatus)" : stderr
throw IDEErrorCode.compilerError(detail)
}
return stdout + (stderr.isEmpty ? "" : "\n\(stderr)")
}
func analyze(script: String) async throws -> [Diagnostic] {
guard !script.isEmpty else { return [] }
var diagnostics: [Diagnostic] = []
let lines = script.components(separatedBy: .newlines)
for (index, line) in lines.enumerated() {
let trimmed = line.trimmingCharacters(in: .whitespaces)
let lineNum = index + 1
if trimmed.contains("try!") {
diagnostics.append(Diagnostic(
line: lineNum, column: 0,
message: "Force try detected. Consider using do/catch.",
severity: .warning
))
}
if trimmed.contains("as!") {
diagnostics.append(Diagnostic(
line: lineNum, column: 0,
message: "Force cast detected. Consider using 'as?'.",
severity: .warning
))
}
if trimmed.contains("!.") || (trimmed.hasSuffix("!") && !trimmed.hasSuffix("\"!") && !trimmed.contains("!=")) {
diagnostics.append(Diagnostic(
line: lineNum, column: 0,
message: "Possible force unwrap. Consider guard/if-let.",
severity: .warning
))
}
if trimmed.hasSuffix("{") && !trimmed.contains(" ") && !trimmed.isEmpty && !trimmed.hasPrefix("//") {
diagnostics.append(Diagnostic(
line: lineNum, column: 0,
message: "Missing space before opening brace.",
severity: .info
))
}
if trimmed.count > 120 {
diagnostics.append(Diagnostic(
line: lineNum, column: 120,
message: "Line exceeds 120 characters (\(trimmed.count) chars).",
severity: .info
))
}
}
// Check balanced braces
var braceDepth = 0
for line in lines {
for ch in line {
if ch == "{" { braceDepth += 1 }
if ch == "}" { braceDepth -= 1 }
}
}
if braceDepth != 0 {
let detail = braceDepth > 0
? "missing \(braceDepth) closing brace(s)"
: "extra \(-braceDepth) closing brace(s)"
diagnostics.append(Diagnostic(
line: lines.count, column: 0,
message: "Unbalanced braces: \(detail).",
severity: .error
))
}
// Check for empty catch blocks
for (index, line) in lines.enumerated() {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed == "} catch {" || trimmed == "catch {" {
if index + 1 < lines.count {
let next = lines[index + 1].trimmingCharacters(in: .whitespaces)
if next == "}" {
diagnostics.append(Diagnostic(
line: index + 1, column: 0,
message: "Empty catch block. Consider logging the error.",
severity: .warning
))
}
}
}
}
return diagnostics
}
func cancel() {
currentProcess?.terminate()
currentProcess = nil
}
}