c118996746
- 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
164 lines
5.7 KiB
Swift
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
|
|
}
|
|
}
|