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
349 lines
12 KiB
Swift
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)
|
|
}
|
|
}
|