Files
CxIDE/Views/IDEContainerView.swift
T
cx-git-agent 6218a6ef28 feat: syntax highlighting, workspace search, git-tree wiring, auto-restore
- NSTextView-based syntax highlighting with regex tokenizer
  - Swift, C/C++, ObjC, JSON keywords, types, strings, comments
  - Theme-aware coloring, debounced re-highlighting
- Workspace-wide search across all source files
  - Grouped results by file with line numbers
  - Scope toggle: current file vs all files
- Git status badges on file tree nodes
  - GitService changes flow to FileNode.gitStatus
- Agent file operations refresh file tree
- Auto-restore last workspace on launch
- All tests passing (0 errors, 0 warnings)
2026-04-21 16:38:42 -05:00

896 lines
35 KiB
Swift

import SwiftUI
// MARK: - IDE Container View (VSCode Layout)
struct IDEContainerView: View {
@ObservedObject var viewModel: EditorViewModel
@State private var sidebarWidth: CGFloat = 260
@State private var panelHeight: CGFloat = 200
@State private var chatPanelWidth: CGFloat = 360
@State private var sidebarDragStart: CGFloat?
@State private var panelDragStart: CGFloat?
@State private var chatDragStart: CGFloat?
@State private var isDraggingSidebar = false
@State private var isDraggingPanel = false
@State private var isDraggingChat = false
var body: some View {
Group {
if viewModel.hasWorkspace {
mainLayout
.overlay { commandPaletteOverlay }
.overlay(alignment: .topTrailing) { notificationStack }
} else {
WelcomeView(workspaceService: viewModel.workspaceService, viewModel: viewModel)
}
}
.preferredColorScheme(.dark)
.frame(minWidth: 900, minHeight: 600)
}
// MARK: - Main Layout
private var mainLayout: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
activityBar
if viewModel.showSidebar {
sidebarPanel
.frame(width: sidebarWidth)
Rectangle()
.fill(isDraggingSidebar ? VSC.focusBorder : VSC.border)
.frame(width: 1)
.contentShape(Rectangle().inset(by: -3))
.gesture(
DragGesture(minimumDistance: 1)
.onChanged { value in
if sidebarDragStart == nil { sidebarDragStart = sidebarWidth }
isDraggingSidebar = true
sidebarWidth = max(180, min(500, (sidebarDragStart ?? 260) + value.translation.width))
}
.onEnded { _ in
isDraggingSidebar = false
sidebarDragStart = nil
}
)
.onHover { inside in
if inside { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() }
}
}
VStack(spacing: 0) {
CodeEditorView(viewModel: viewModel)
if viewModel.showPanel {
Rectangle()
.fill(isDraggingPanel ? VSC.focusBorder : VSC.border)
.frame(height: 1)
.contentShape(Rectangle().inset(by: -3))
.gesture(
DragGesture(minimumDistance: 1)
.onChanged { value in
if panelDragStart == nil { panelDragStart = panelHeight }
isDraggingPanel = true
panelHeight = max(100, min(500, (panelDragStart ?? 200) - value.translation.height))
}
.onEnded { _ in
isDraggingPanel = false
panelDragStart = nil
}
)
.onHover { inside in
if inside { NSCursor.resizeUpDown.push() } else { NSCursor.pop() }
}
ConsoleView(viewModel: viewModel)
.frame(height: panelHeight)
}
}
// MARK: - Right-Side Chat Panel
if viewModel.showChatPanel {
Rectangle()
.fill(isDraggingChat ? VSC.focusBorder : VSC.border)
.frame(width: 1)
.contentShape(Rectangle().inset(by: -3))
.gesture(
DragGesture(minimumDistance: 1)
.onChanged { value in
if chatDragStart == nil { chatDragStart = chatPanelWidth }
isDraggingChat = true
chatPanelWidth = max(280, min(600, (chatDragStart ?? 360) - value.translation.width))
}
.onEnded { _ in
isDraggingChat = false
chatDragStart = nil
}
)
.onHover { inside in
if inside { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() }
}
ChatPanelView(viewModel: viewModel)
.frame(width: chatPanelWidth)
}
}
statusBar
}
.background(VSC.editorBg)
}
// MARK: - Activity Bar
private var activityBar: some View {
VStack(spacing: 0) {
VStack(spacing: 2) {
activityBarButton(.explorer)
activityBarButton(.search)
activityBarButton(.git)
activityBarButton(.debug)
activityBarButton(.agent)
}
.padding(.top, 8)
Spacer()
VStack(spacing: 2) {
activityBarButton(.snippets)
activityBarButton(.settings)
}
.padding(.bottom, 8)
}
.frame(width: 48)
.background(VSC.activityBarBg)
}
private func activityBarButton(_ panel: ActivityPanel) -> some View {
let isActive = viewModel.activePanel == panel && viewModel.showSidebar
return Button(action: {
if viewModel.activePanel == panel {
viewModel.showSidebar.toggle()
} else {
viewModel.activePanel = panel
viewModel.showSidebar = true
}
}) {
ZStack {
Image(systemName: panel.iconName)
.font(.system(size: 22))
.foregroundColor(isActive ? VSC.activityActive : VSC.activityInactive)
.frame(width: 48, height: 48)
if isActive {
HStack {
Rectangle()
.fill(Color.white)
.frame(width: 2, height: 24)
Spacer()
}
}
if panel == .git, gitBadgeCount > 0 {
badgeOverlay(gitBadgeCount)
}
if panel == .debug, viewModel.diagnostics.count > 0 {
badgeOverlay(viewModel.diagnostics.count)
}
}
}
.buttonStyle(.plain)
}
private var gitBadgeCount: Int {
let s = viewModel.gitService.status
return s.modified + s.untracked + s.staged + s.conflicted
}
private func badgeOverlay(_ count: Int) -> some View {
VStack {
HStack {
Spacer()
Text("\(min(count, 99))")
.font(.system(size: 9, weight: .bold))
.foregroundColor(.white)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(VSC.badgeBg)
.cornerRadius(8)
}
Spacer()
}
.padding(.top, 4)
.padding(.trailing, 4)
}
// MARK: - Sidebar Panel
private var sidebarPanel: some View {
VStack(spacing: 0) {
HStack {
Text(viewModel.activePanel.rawValue.uppercased())
.font(.system(size: 11, weight: .semibold))
.foregroundColor(VSC.dimText)
.tracking(0.8)
Spacer()
if viewModel.activePanel == .explorer {
HStack(spacing: 4) {
sidebarHeaderButton("doc.badge.plus", help: "New File")
sidebarHeaderButton("folder.badge.plus", help: "New Folder")
sidebarHeaderButton("arrow.clockwise", help: "Refresh") {
viewModel.workspaceService.refreshFileTree()
}
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
switch viewModel.activePanel {
case .explorer:
FileTreeView(workspaceService: viewModel.workspaceService, viewModel: viewModel)
case .search:
SearchSidebarView(viewModel: viewModel)
case .git:
GitSidebarView(gitService: viewModel.gitService, viewModel: viewModel)
case .snippets:
SnippetsSidebarView(viewModel: viewModel)
case .debug:
DebugSidebarView(viewModel: viewModel)
case .agent:
AgentSidebarView(viewModel: viewModel)
case .chat:
// Chat is on the right-side panel, not in sidebar
EmptyView()
case .settings:
SettingsSidebarView(viewModel: viewModel)
}
}
.background(VSC.sidebarBg)
}
private func sidebarHeaderButton(_ icon: String, help: String, action: @escaping () -> Void = {}) -> some View {
Button(action: action) {
Image(systemName: icon)
.font(.system(size: 12))
.foregroundColor(VSC.dimText)
}
.buttonStyle(.plain)
.help(help)
}
// MARK: - Status Bar
private var statusBar: some View {
HStack(spacing: 0) {
HStack(spacing: 12) {
if viewModel.gitService.isGitRepo {
statusBarItem {
HStack(spacing: 4) {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: 10))
Text(viewModel.gitService.status.branch)
.font(.system(size: 11))
}
}
}
statusBarItem {
HStack(spacing: 8) {
HStack(spacing: 2) {
Image(systemName: "xmark.circle")
.font(.system(size: 10))
Text("\(viewModel.errorCount)")
.font(.system(size: 11))
}
HStack(spacing: 2) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 10))
Text("\(viewModel.warningCount)")
.font(.system(size: 11))
}
}
}
}
.padding(.leading, 8)
Spacer()
HStack(spacing: 0) {
if let tab = viewModel.activeTab {
statusBarItem {
Text("Ln \(tab.cursorLine), Col \(tab.cursorColumn)")
.font(.system(size: 11))
}
}
statusBarItem {
Text("Spaces: 4").font(.system(size: 11))
}
statusBarItem {
Text("UTF-8").font(.system(size: 11))
}
if let tab = viewModel.activeTab {
statusBarItem {
Text(tab.language.displayName).font(.system(size: 11))
}
}
}
.padding(.trailing, 8)
}
.frame(height: 22)
.background(viewModel.hasWorkspace ? VSC.statusBarBg : VSC.statusBarNoFolder)
.foregroundColor(.white.opacity(0.9))
}
private func statusBarItem<Content: View>(@ViewBuilder content: () -> Content) -> some View {
content()
.padding(.horizontal, 6)
.frame(height: 22)
.contentShape(Rectangle())
}
// MARK: - Command Palette
@ViewBuilder
private var commandPaletteOverlay: some View {
if viewModel.showCommandPalette {
ZStack {
Color.black.opacity(0.25)
.ignoresSafeArea()
.onTapGesture {
viewModel.showCommandPalette = false
viewModel.commandSearch = ""
}
VStack {
VStack(spacing: 0) {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.font(.system(size: 13))
.foregroundColor(VSC.dimText)
TextField("Type a command...", text: $viewModel.commandSearch)
.textFieldStyle(.plain)
.font(.system(size: 14))
.foregroundColor(.white)
.onSubmit {
if let first = viewModel.filteredCommands.first {
first.action()
viewModel.showCommandPalette = false
viewModel.commandSearch = ""
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
Divider().background(VSC.border)
ScrollView {
LazyVStack(spacing: 0) {
ForEach(viewModel.filteredCommands) { cmd in
Button {
cmd.action()
viewModel.showCommandPalette = false
viewModel.commandSearch = ""
} label: {
HStack(spacing: 10) {
Image(systemName: cmd.icon)
.font(.system(size: 12))
.foregroundColor(VSC.dimText)
.frame(width: 20)
Text(cmd.title)
.font(.system(size: 13))
.foregroundColor(VSC.text)
Spacer()
if let shortcut = cmd.shortcut {
Text(shortcut)
.font(.system(size: 11, design: .monospaced))
.foregroundColor(VSC.dimText)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}
.frame(maxHeight: 300)
}
.frame(width: 500)
.background(VSC.sidebarBg)
.cornerRadius(6)
.overlay(RoundedRectangle(cornerRadius: 6).stroke(VSC.border, lineWidth: 1))
.shadow(color: .black.opacity(0.5), radius: 16, y: 8)
Spacer()
}
.padding(.top, 60)
}
}
}
// MARK: - Notifications
@ViewBuilder
private var notificationStack: some View {
VStack(spacing: 8) {
ForEach(viewModel.notifications) { notification in
NotificationBanner(notification: notification) {
viewModel.dismissNotification(notification)
}
.transition(.move(edge: .trailing).combined(with: .opacity))
}
}
.padding(.top, 40)
.padding(.trailing, 16)
.animation(.spring(response: 0.3), value: viewModel.notifications.count)
}
}
// MARK: - Search Sidebar
struct SearchSidebarView: View {
@ObservedObject var viewModel: EditorViewModel
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
TextField("Search", text: $viewModel.searchState.query)
.textFieldStyle(.plain)
.font(.system(size: 12))
.foregroundColor(.white)
.onSubmit { runSearch() }
}
.padding(6)
.background(VSC.inputBg)
.cornerRadius(4)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(VSC.inputBorder, lineWidth: 1))
HStack(spacing: 6) {
Image(systemName: "arrow.2.squarepath")
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
TextField("Replace", text: $viewModel.searchState.replacement)
.textFieldStyle(.plain)
.font(.system(size: 12))
.foregroundColor(.white)
}
.padding(6)
.background(VSC.inputBg)
.cornerRadius(4)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(VSC.inputBorder, lineWidth: 1))
HStack(spacing: 8) {
searchToggle("Aa", isOn: $viewModel.searchState.isCaseSensitive, help: "Match Case")
searchToggle(".*", isOn: $viewModel.searchState.isRegex, help: "Use Regex")
searchToggle("W", isOn: $viewModel.searchState.isWholeWord, help: "Whole Word")
Spacer()
// Scope picker
Picker("", selection: $viewModel.searchState.scope) {
ForEach(SearchState.SearchScope.allCases, id: \.self) { scope in
Text(scope.rawValue).tag(scope)
}
}
.pickerStyle(.segmented)
.frame(width: 160)
.controlSize(.mini)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
Divider().background(VSC.border)
// Results
if viewModel.isSearchingWorkspace {
HStack(spacing: 8) {
ProgressView().scaleEffect(0.6)
Text("Searching workspace...")
.font(.system(size: 10))
.foregroundColor(VSC.dimText)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
if viewModel.searchState.scope == .allFiles {
workspaceResults
} else {
currentFileResults
}
}
}
private var currentFileResults: some View {
VStack(spacing: 0) {
if viewModel.searchState.matchCount > 0 {
HStack(spacing: 8) {
Text("\(viewModel.searchState.matchCount) results in current file")
.font(.system(size: 10))
.foregroundColor(VSC.dimText)
Spacer()
Button("Replace Next") { viewModel.replaceNext() }
.font(.system(size: 10))
.controlSize(.mini)
Button("Replace All") { viewModel.replaceAll() }
.font(.system(size: 10))
.controlSize(.mini)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
} else if !viewModel.searchState.query.isEmpty {
Text("No results found.")
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
.padding(12)
}
Spacer()
}
}
private var workspaceResults: some View {
Group {
if !viewModel.workspaceSearchResults.isEmpty {
HStack {
Text("\(viewModel.workspaceSearchResults.count) results")
.font(.system(size: 10))
.foregroundColor(VSC.dimText)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
let grouped = Dictionary(grouping: viewModel.workspaceSearchResults) { $0.fileURL }
ForEach(grouped.keys.sorted(by: { $0.path < $1.path }), id: \.self) { fileURL in
searchFileGroup(fileURL: fileURL, results: grouped[fileURL] ?? [])
}
}
}
} else if !viewModel.searchState.query.isEmpty && !viewModel.isSearchingWorkspace {
Text("No results found in workspace.")
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
.padding(12)
Spacer()
} else {
Spacer()
}
}
}
private func searchFileGroup(fileURL: URL, results: [WorkspaceSearchResult]) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 6) {
Image(systemName: "doc.text")
.font(.system(size: 9))
.foregroundColor(VSC.dimText)
Text(fileURL.lastPathComponent)
.font(.system(size: 11, weight: .semibold))
.foregroundColor(VSC.text)
Text("\(results.count)")
.font(.system(size: 9))
.foregroundColor(VSC.dimText)
.padding(.horizontal, 4)
.background(VSC.inputBg)
.cornerRadius(4)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(VSC.inputBg.opacity(0.5))
ForEach(results) { result in
Button(action: { viewModel.openFile(at: result.fileURL) }) {
HStack(spacing: 6) {
Text("\(result.line)")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(VSC.dimText)
.frame(width: 28, alignment: .trailing)
Text(result.lineContent.trimmingCharacters(in: .whitespaces))
.font(.system(size: 10, design: .monospaced))
.foregroundColor(VSC.text)
.lineLimit(1)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 2)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}
private func runSearch() {
if viewModel.searchState.scope == .allFiles {
viewModel.searchWorkspace()
} else {
viewModel.performSearch()
}
}
private func searchToggle(_ label: String, isOn: Binding<Bool>, help: String) -> some View {
Button(action: { isOn.wrappedValue.toggle(); runSearch() }) {
Text(label)
.font(.system(size: 10, weight: .medium, design: .monospaced))
.foregroundColor(isOn.wrappedValue ? .white : VSC.dimText)
.frame(width: 22, height: 18)
.background(isOn.wrappedValue ? VSC.accentBlue.opacity(0.5) : Color.clear)
.cornerRadius(3)
.overlay(RoundedRectangle(cornerRadius: 3).stroke(VSC.inputBorder, lineWidth: 1))
}
.buttonStyle(.plain)
.help(help)
}
}
// MARK: - Snippets Sidebar
struct SnippetsSidebarView: View {
@ObservedObject var viewModel: EditorViewModel
private var categories: [String] {
Array(Set(CodeSnippet.builtIn.map(\.category))).sorted()
}
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(categories, id: \.self) { category in
DisclosureGroup {
ForEach(CodeSnippet.builtIn.filter { $0.category == category }) { snippet in
Button(action: { viewModel.insertSnippet(snippet) }) {
HStack(spacing: 8) {
Image(systemName: "text.insert")
.font(.system(size: 10))
.foregroundColor(.orange)
.frame(width: 14)
Text(snippet.title)
.font(.system(size: 11))
.foregroundColor(VSC.text)
Spacer()
}
.padding(.vertical, 3)
.padding(.leading, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
} label: {
Text(category.uppercased())
.font(.system(size: 10, weight: .semibold))
.foregroundColor(VSC.dimText)
.tracking(0.5)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
}
}
}
}
}
// MARK: - Debug Sidebar
struct DebugSidebarView: View {
@ObservedObject var viewModel: EditorViewModel
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
sectionHeader("BREAKPOINTS", count: viewModel.breakpoints.count)
if viewModel.breakpoints.isEmpty {
emptyMessage("No breakpoints set")
} else {
ForEach(viewModel.breakpoints) { bp in
HStack(spacing: 6) {
Image(systemName: bp.isEnabled ? "circle.fill" : "circle")
.font(.system(size: 8))
.foregroundColor(.red)
Text(bp.file)
.font(.system(size: 11))
.foregroundColor(VSC.text)
.lineLimit(1)
Spacer()
Text(":\(bp.line)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(VSC.dimText)
}
.padding(.horizontal, 16)
.padding(.vertical, 3)
}
}
Divider().background(VSC.border).padding(.vertical, 8)
sectionHeader("C++ ANALYSIS", count: nil)
VStack(spacing: 6) {
analysisButton("Run Analysis", icon: "play.circle", action: viewModel.runCppAnalysis)
analysisButton("Checksum", icon: "number", action: viewModel.runChecksum)
analysisButton("Complexity", icon: "chart.bar", action: viewModel.runComplexityCheck)
}
.padding(.horizontal, 16)
.padding(.top, 4)
Spacer()
}
}
}
private func sectionHeader(_ title: String, count: Int?) -> some View {
HStack {
Text(title)
.font(.system(size: 10, weight: .semibold))
.foregroundColor(VSC.dimText)
.tracking(0.5)
if let count = count, count > 0 {
Text("\(count)")
.font(.system(size: 9, weight: .bold))
.foregroundColor(.white)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(VSC.badgeBg)
.cornerRadius(6)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 6)
}
private func emptyMessage(_ text: String) -> some View {
Text(text)
.font(.system(size: 11))
.foregroundColor(VSC.dimText)
.padding(.horizontal, 16)
.padding(.vertical, 4)
}
private func analysisButton(_ title: String, icon: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 6) {
Image(systemName: icon).font(.system(size: 11)).frame(width: 16)
Text(title).font(.system(size: 11))
Spacer()
}
.foregroundColor(VSC.text)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(VSC.inputBg)
.cornerRadius(4)
}
.buttonStyle(.plain)
}
}
// MARK: - Settings Sidebar
struct SettingsSidebarView: View {
@ObservedObject var viewModel: EditorViewModel
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
settingsGroup("Theme") {
ForEach(IDETheme.allThemes) { theme in
Button(action: { viewModel.currentTheme = theme }) {
HStack(spacing: 8) {
Circle()
.fill(theme.editorBackground)
.frame(width: 14, height: 14)
.overlay(Circle().stroke(Color.white.opacity(0.2), lineWidth: 1))
Text(theme.name)
.font(.system(size: 11))
.foregroundColor(VSC.text)
Spacer()
if viewModel.currentTheme == theme {
Image(systemName: "checkmark")
.font(.system(size: 10))
.foregroundColor(VSC.accentBlue)
}
}
.padding(.vertical, 3)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
settingsGroup("Editor") {
HStack {
Text("Font Size").font(.system(size: 11)).foregroundColor(VSC.text)
Spacer()
HStack(spacing: 8) {
Button(action: viewModel.decreaseFontSize) {
Image(systemName: "minus").font(.system(size: 10))
}.buttonStyle(.plain)
Text("\(Int(viewModel.fontSize))")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(VSC.text)
.frame(width: 24)
Button(action: viewModel.increaseFontSize) {
Image(systemName: "plus").font(.system(size: 10))
}.buttonStyle(.plain)
}
.foregroundColor(VSC.dimText)
}
}
if let workspace = viewModel.workspaceService.currentWorkspace {
settingsGroup("Workspace") {
infoRow("Name", value: workspace.name)
infoRow("Path", value: workspace.rootURL.path)
infoRow("Files", value: "\(viewModel.workspaceService.fileTree.count) items")
}
}
settingsGroup("Shortcuts") {
shortcutRow("Command Palette", shortcut: "\u{21e7}\u{2318}P")
shortcutRow("Find", shortcut: "\u{2318}F")
shortcutRow("New Tab", shortcut: "\u{2318}N")
shortcutRow("Save", shortcut: "\u{2318}S")
shortcutRow("Run", shortcut: "\u{2318}R")
shortcutRow("Toggle Sidebar", shortcut: "\u{2318}B")
shortcutRow("Toggle Panel", shortcut: "\u{2318}J")
}
settingsGroup("About") {
infoRow("Version", value: "1.0.0")
infoRow("Engine", value: "Swift + C++ Hybrid")
infoRow("Agent", value: "MCP \(viewModel.agentService.toolCount) tools")
}
}
.padding(16)
}
}
private func settingsGroup<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title.uppercased())
.font(.system(size: 10, weight: .semibold))
.foregroundColor(VSC.dimText)
.tracking(0.5)
content()
}
}
private func infoRow(_ label: String, value: String) -> some View {
HStack {
Text(label).font(.system(size: 11)).foregroundColor(VSC.dimText)
Spacer()
Text(value).font(.system(size: 11)).foregroundColor(VSC.text).lineLimit(1).truncationMode(.middle)
}
}
private func shortcutRow(_ title: String, shortcut: String) -> some View {
HStack {
Text(title).font(.system(size: 11)).foregroundColor(VSC.text)
Spacer()
Text(shortcut)
.font(.system(size: 10, design: .monospaced))
.foregroundColor(VSC.dimText)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(VSC.inputBg)
.cornerRadius(3)
}
}
}