// CxLLM Studio — iOS Application // Views/Agent/AgentView.swift — Agent workspace for iOS import SwiftUI import CxCode struct AgentView: View { @Environment(AgentService.self) private var agent @State private var taskInput = "" @State private var maxSteps = 10 @State private var output = "" @State private var selectedTool: AgentToolInfo? @State private var toolArgs = "{}" @State private var toolResult = "" @State private var showToolBrowser = false private let agentGreen = Color(red: 0.46, green: 0.72, blue: 0.0) var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { statusSection autopilotSection quickTasksSection if !output.isEmpty { outputSection } toolExecutionSection if !agent.history.isEmpty { historySection } } .padding() } .navigationTitle("Agent") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showToolBrowser = true } label: { Label("Tools", systemImage: "wrench.and.screwdriver") } } ToolbarItem(placement: .topBarTrailing) { statusPill } } .sheet(isPresented: $showToolBrowser) { toolBrowserSheet } .task { await agent.initialize() } } // MARK: - Status private var statusSection: some View { HStack(spacing: 12) { statCard("Tools", "\(agent.tools.count)", "wrench", agentGreen) statCard("Runs", "\(agent.totalExecutions)", "play.circle", .blue) statCard("Calls", "\(agent.totalToolCalls)", "arrow.right.circle", .purple) } } private func statCard(_ label: String, _ value: String, _ icon: String, _ color: Color) -> some View { VStack(spacing: 4) { Image(systemName: icon).font(.system(size: 18)).foregroundStyle(color) Text(value).font(.system(size: 20, weight: .bold, design: .rounded)) Text(label).font(.caption2).foregroundStyle(.secondary) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(color.opacity(0.06)) .clipShape(RoundedRectangle(cornerRadius: 12)) } // MARK: - Autopilot private var autopilotSection: some View { VStack(alignment: .leading, spacing: 8) { Label("Autopilot", systemImage: "bolt.fill") .font(.headline).foregroundStyle(agentGreen) TextField("Describe a task...", text: $taskInput, axis: .vertical) .lineLimit(2...5) .textFieldStyle(.roundedBorder) HStack { HStack(spacing: 4) { Text("Max steps:").font(.caption).foregroundStyle(.secondary) TextField("", value: $maxSteps, format: .number) .textFieldStyle(.roundedBorder) .frame(width: 50) .font(.caption) } Spacer() Button { runAutopilot() } label: { HStack(spacing: 4) { if agent.isRunning { ProgressView().controlSize(.small) } else { Image(systemName: "play.fill") } Text(agent.isRunning ? "Running..." : "Execute") } .font(.subheadline.weight(.semibold)) } .buttonStyle(.borderedProminent).tint(agentGreen) .disabled(taskInput.isEmpty || agent.isRunning || !agent.isHealthy) } } } // MARK: - Quick Tasks private var quickTasksSection: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { quickTaskChip("Analyze project", "magnifyingglass") quickTaskChip("Fix bugs", "ant") quickTaskChip("Write tests", "checkmark.circle") quickTaskChip("Refactor", "arrow.triangle.2.circlepath") quickTaskChip("Document", "doc.text") } } } private func quickTaskChip(_ text: String, _ icon: String) -> some View { Button { taskInput = text } label: { Label(text, systemImage: icon).font(.caption) .padding(.horizontal, 10).padding(.vertical, 6) .background(Color.primary.opacity(0.05)) .clipShape(Capsule()) }.buttonStyle(.plain) } // MARK: - Output private var outputSection: some View { VStack(alignment: .leading, spacing: 6) { HStack { Label("Output", systemImage: "text.alignleft") .font(.headline) Spacer() Button { UIPasteboard.general.string = output } label: { Image(systemName: "doc.on.doc").font(.caption) } } Text(output) .font(.system(.caption, design: .monospaced)) .textSelection(.enabled) .padding(10) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.primary.opacity(0.03)) .clipShape(RoundedRectangle(cornerRadius: 8)) } } // MARK: - Tool Execution private var toolExecutionSection: some View { VStack(alignment: .leading, spacing: 8) { Label("Direct Tool Call", systemImage: "terminal") .font(.headline) if let tool = selectedTool { HStack { Label(tool.name, systemImage: "wrench").font(.subheadline.weight(.medium)) Spacer() Button("Change") { showToolBrowser = true } .font(.caption).buttonStyle(.bordered).controlSize(.small) } if let desc = tool.description { Text(desc).font(.caption).foregroundStyle(.secondary) } } else { Button { showToolBrowser = true } label: { Label("Select a tool", systemImage: "plus.circle") .frame(maxWidth: .infinity) .padding(.vertical, 8) } .buttonStyle(.bordered) } Text("Arguments (JSON)").font(.caption).foregroundStyle(.secondary) TextEditor(text: $toolArgs) .font(.system(.caption, design: .monospaced)) .scrollContentBackground(.hidden) .frame(minHeight: 60, maxHeight: 120) .padding(8) .background(Color.primary.opacity(0.03)) .clipShape(RoundedRectangle(cornerRadius: 8)) Button { execTool() } label: { HStack(spacing: 4) { if agent.isExecutingTool { ProgressView().controlSize(.small) } else { Image(systemName: "play.fill") } Text("Execute") } .font(.subheadline.weight(.medium)) } .buttonStyle(.borderedProminent).controlSize(.small) .disabled(selectedTool == nil || agent.isExecutingTool) if !toolResult.isEmpty { VStack(alignment: .leading, spacing: 4) { HStack { Text("Result").font(.caption.weight(.semibold)).foregroundStyle(.secondary) Spacer() Button { UIPasteboard.general.string = toolResult } label: { Image(systemName: "doc.on.doc").font(.caption2) } } Text(toolResult) .font(.system(.caption, design: .monospaced)) .textSelection(.enabled) .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.primary.opacity(0.03)) .clipShape(RoundedRectangle(cornerRadius: 8)) } } } } // MARK: - History private var historySection: some View { VStack(alignment: .leading, spacing: 8) { HStack { Label("History", systemImage: "clock").font(.headline) Spacer() Button("Clear") { agent.clearHistory() } .font(.caption).buttonStyle(.bordered).controlSize(.mini) } ForEach(agent.history) { h in HStack(spacing: 10) { Image(systemName: h.success ? "checkmark.circle.fill" : "xmark.circle.fill") .foregroundStyle(h.success ? .green : .red) .font(.body) VStack(alignment: .leading, spacing: 2) { Text(h.task).font(.subheadline.weight(.medium)).lineLimit(1) Text("\(h.steps.count) steps · \(String(format: "%.1fs", h.duration))") .font(.caption2).foregroundStyle(.secondary) } Spacer() Text(h.time, format: .dateTime.hour().minute()) .font(.caption2.monospacedDigit()).foregroundStyle(.tertiary) } .padding(10) .background(Color.primary.opacity(0.02)) .clipShape(RoundedRectangle(cornerRadius: 8)) } } } // MARK: - Tool Browser Sheet private var toolBrowserSheet: some View { NavigationStack { List(agent.tools, id: \.name) { tool in Button { selectedTool = tool showToolBrowser = false } label: { VStack(alignment: .leading, spacing: 2) { Text(tool.name).font(.subheadline.weight(.medium)) if let desc = tool.description { Text(desc).font(.caption).foregroundStyle(.secondary).lineLimit(2) } } } } .navigationTitle("Agent Tools") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { showToolBrowser = false } } } } } // MARK: - Components private var statusPill: some View { HStack(spacing: 4) { Circle().fill(agent.isHealthy ? Color.green : Color.red.opacity(0.5)).frame(width: 6, height: 6) Text(agent.isHealthy ? "Ready" : "Offline").font(.caption2.weight(.semibold)) } .padding(.horizontal, 8).padding(.vertical, 4) .background(agent.isHealthy ? Color.green.opacity(0.08) : Color.red.opacity(0.08)) .clipShape(Capsule()) } // MARK: - Actions private func runAutopilot() { guard !taskInput.isEmpty else { return } output = "" Task { do { let execution = try await agent.runAutopilot(task: taskInput, maxSteps: maxSteps) output = execution.output } catch { output = "Error: \(error.localizedDescription)" } } } private func execTool() { guard let tool = selectedTool else { return } toolResult = "" Task { do { let args = parseJSON(toolArgs) toolResult = try await agent.callTool(name: tool.name, arguments: args) } catch { toolResult = "Error: \(error.localizedDescription)" } } } private func parseJSON(_ s: String) -> [String: Any] { guard let d = s.data(using: .utf8), let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any] else { return [:] } return o } }