// // ConsoleView.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import SwiftUI /// Command console: scrollback of executed commands with their rendered /// output, plus an input line with ↑/↓ history recall (CLAUDE.md §7). struct ConsoleView: View { @Environment(SessionModel.self) private var model @State private var input = "" @State private var historyCursor: Int? @FocusState private var inputFocused: Bool var body: some View { VStack(spacing: 0) { ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 10) { ForEach(model.entries) { entry in VStack(alignment: .leading, spacing: 2) { if !entry.command.isEmpty { Text(". \(entry.command)") .foregroundStyle(.secondary) } if !entry.output.isEmpty { Text(entry.output) .foregroundStyle( entry.isError ? Color.red : Color.primary ) .textSelection(.enabled) } } .id(entry.id) } } .padding(12) .frame(maxWidth: .infinity, alignment: .leading) } .onChange(of: model.entries) { if let last = model.entries.last { proxy.scrollTo(last.id, anchor: .bottom) } } } Divider() HStack(spacing: 8) { Text(".") .foregroundStyle(.secondary) TextField("Type a command — e.g. use sales.parquet", text: $input) .textFieldStyle(.plain) .focused($inputFocused) .onSubmit(submit) .onKeyPress(.upArrow) { recallHistory(step: -1) } .onKeyPress(.downArrow) { recallHistory(step: 1) } if model.isRunning { ProgressView() .controlSize(.small) } } .padding(10) } .fontDesign(.monospaced) .font(.system(size: 12)) .onAppear { inputFocused = true } } private func submit() { let command = input input = "" historyCursor = nil Task { await model.run(command) } } private func recallHistory(step: Int) -> KeyPress.Result { let history = model.history guard !history.isEmpty else { return .ignored } var cursor = historyCursor ?? history.count cursor = min(max(cursor + step, 0), history.count) historyCursor = cursor input = cursor < history.count ? history[cursor] : "" return .handled } } /// Sidebar: variables of the working dataset with type and missing count /// (CLAUDE.md §7, pane 5). struct VariablesSidebar: View { @Environment(SessionModel.self) private var model var body: some View { List { Section("Variables (\(model.observationCount) obs)") { ForEach(model.variables) { variable in HStack { Text(variable.name) .fontDesign(.monospaced) Spacer() if variable.missingCount > 0 { Text("\(variable.missingCount) mi") .foregroundStyle(.orange) .font(.caption) } Text(variable.type) .foregroundStyle(.secondary) .font(.caption) } } } } .listStyle(.sidebar) } }