Files
CxIDE/Views/ConsoleView.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

349 lines
12 KiB
Swift

import SwiftUI
// MARK: - Console View (VSCode Bottom Panel)
struct ConsoleView: View {
@ObservedObject var viewModel: EditorViewModel
@State private var filterText: String = ""
@State private var selectedLevel: ConsoleEntry.Level?
@State private var agentInput: String = ""
var body: some View {
VStack(spacing: 0) {
panelHeader
panelContent
}
.background(VSC.panelBg)
}
// MARK: - Panel Header (Tabs)
private var panelHeader: some View {
HStack(spacing: 0) {
HStack(spacing: 0) {
panelTab("PROBLEMS", panel: .problems, badge: viewModel.diagnostics.count)
panelTab("OUTPUT", panel: .output, badge: 0)
panelTab("DEBUG CONSOLE", panel: .console, badge: 0)
panelTab("TERMINAL", panel: .terminal, badge: 0)
panelTab("AGENT", panel: .agent, badge: viewModel.agentService.messageCount)
}
Spacer()
// Panel actions
HStack(spacing: 8) {
if viewModel.bottomPanel == .console {
Button(action: viewModel.clearConsole) {
Image(systemName: "trash")
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
}
.buttonStyle(.plain)
.help("Clear Console")
}
Button(action: { viewModel.showPanel = false }) {
Image(systemName: "xmark")
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
}
.buttonStyle(.plain)
.help("Close Panel")
}
.padding(.trailing, 12)
}
.frame(height: 35)
.background(VSC.panelBg)
.overlay(alignment: .bottom) {
Rectangle().fill(VSC.border).frame(height: 1)
}
}
private func panelTab(_ title: String, panel: EditorViewModel.BottomPanel, badge: Int) -> some View {
let isActive = viewModel.bottomPanel == panel
return Button(action: { viewModel.bottomPanel = panel }) {
HStack(spacing: 4) {
Text(title)
.font(.system(size: 11, weight: isActive ? .semibold : .regular))
.foregroundColor(isActive ? VSC.text : VSC.dimText)
if badge > 0 {
Text("\(badge)")
.font(.system(size: 9, weight: .bold))
.foregroundColor(.white)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(panel == .problems ? VSC.errorFg.opacity(0.8) : VSC.badgeBg)
.cornerRadius(6)
}
}
.padding(.horizontal, 12)
.frame(height: 35)
.overlay(alignment: .bottom) {
if isActive {
Rectangle()
.fill(VSC.accentBlue)
.frame(height: 1)
}
}
}
.buttonStyle(.plain)
}
// MARK: - Panel Content
@ViewBuilder
private var panelContent: some View {
switch viewModel.bottomPanel {
case .problems:
problemsPanel
case .output:
outputPanel
case .console:
consolePanel
case .terminal:
TerminalView(viewModel: viewModel)
case .agent:
agentPanel
case .debug:
debugPanel
}
}
// MARK: - Problems Panel
private var problemsPanel: some View {
Group {
if viewModel.diagnostics.isEmpty {
panelEmptyState("No problems detected", icon: "checkmark.circle")
} else {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.diagnostics) { diag in
HStack(spacing: 8) {
Image(systemName: diag.severity.iconName)
.font(.system(size: 11))
.foregroundColor(diag.severity.color)
.frame(width: 16)
Text(diag.message)
.font(.system(size: 12))
.foregroundColor(VSC.text)
.lineLimit(2)
Spacer()
Text("\(diag.line):\(diag.column)")
.font(.system(size: 10, design: .monospaced))
.foregroundColor(VSC.dimText)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
.contentShape(Rectangle())
}
}
}
}
}
}
// MARK: - Output Panel
private var outputPanel: some View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Text(viewModel.output.isEmpty ? "No output yet." : viewModel.output)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(viewModel.output.isEmpty ? VSC.dimText : VSC.text)
.textSelection(.enabled)
.padding(12)
.id("output-bottom")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.onChange(of: viewModel.output) { _, _ in
proxy.scrollTo("output-bottom", anchor: .bottom)
}
}
}
// MARK: - Console Panel
private var consolePanel: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.consoleEntries) { entry in
HStack(alignment: .top, spacing: 8) {
Text(entry.formattedTimestamp)
.font(.system(size: 10, design: .monospaced))
.foregroundColor(VSC.dimText)
.frame(width: 80, alignment: .leading)
Image(systemName: entry.level.iconName)
.font(.system(size: 10))
.foregroundColor(entry.level.color)
.frame(width: 14)
Text(entry.message)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(entry.level.color)
.textSelection(.enabled)
}
.padding(.horizontal, 12)
.padding(.vertical, 2)
.id(entry.id)
}
}
}
.onChange(of: viewModel.consoleEntries.count) { _, _ in
if let last = viewModel.consoleEntries.last {
proxy.scrollTo(last.id, anchor: .bottom)
}
}
}
}
// MARK: - Debug Panel
private var debugPanel: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
if !viewModel.cppAnalysis.isEmpty {
Text(viewModel.cppAnalysis)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(VSC.text)
.textSelection(.enabled)
.padding(12)
} else {
panelEmptyState("No debug output", icon: "ant")
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// MARK: - Agent Panel
private var agentPanel: some View {
VStack(spacing: 0) {
// Agent status bar
HStack(spacing: 8) {
Circle()
.fill(viewModel.agentService.status.color)
.frame(width: 8, height: 8)
Text(viewModel.agentService.status.displayText)
.font(.system(size: 11))
.foregroundColor(VSC.text)
Spacer()
Text("\(viewModel.agentService.toolCount) tools")
.font(.system(size: 10))
.foregroundColor(VSC.dimText)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(VSC.sidebarBg)
// Messages
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(viewModel.agentService.messages) { msg in
agentMessageRow(msg)
.id(msg.id)
}
}
.padding(12)
}
.onChange(of: viewModel.agentService.messages.count) { _, _ in
if let last = viewModel.agentService.messages.last {
proxy.scrollTo(last.id, anchor: .bottom)
}
}
}
// Input bar
HStack(spacing: 8) {
Image(systemName: "bubble.left")
.font(.system(size: 12))
.foregroundColor(VSC.dimText)
TextField("Ask the agent...", text: $agentInput)
.textFieldStyle(.plain)
.font(.system(size: 12))
.foregroundColor(.white)
.onSubmit { submitAgentMessage() }
Button(action: submitAgentMessage) {
Image(systemName: "paperplane.fill")
.font(.system(size: 12))
.foregroundColor(agentInput.isEmpty ? VSC.dimText : VSC.accentBlue)
}
.buttonStyle(.plain)
.disabled(agentInput.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(VSC.sidebarBg)
}
}
private func agentMessageRow(_ msg: AgentMessage) -> some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: msg.role.iconName)
.font(.system(size: 11))
.foregroundColor(msg.role.color)
.frame(width: 16)
VStack(alignment: .leading, spacing: 2) {
if let tool = msg.toolName {
Text(tool)
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.orange)
}
Text(msg.content)
.font(.system(size: 12, design: msg.role == .tool ? .monospaced : .default))
.foregroundColor(msg.isError ? VSC.errorFg : VSC.text)
.textSelection(.enabled)
Text(msg.formattedTimestamp)
.font(.system(size: 9))
.foregroundColor(VSC.dimText)
}
}
.padding(8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(msg.role == .user ? VSC.listHover : Color.clear)
.cornerRadius(4)
}
private func submitAgentMessage() {
let text = agentInput.trimmingCharacters(in: .whitespaces)
guard !text.isEmpty else { return }
agentInput = ""
viewModel.sendAgentMessage(text)
}
// MARK: - Empty State
private func panelEmptyState(_ message: String, icon: String) -> some View {
VStack(spacing: 8) {
Spacer()
Image(systemName: icon)
.font(.system(size: 28))
.foregroundColor(VSC.dimText.opacity(0.4))
Text(message)
.font(.system(size: 12))
.foregroundColor(VSC.dimText)
Spacer()
}
.frame(maxWidth: .infinity)
}
}