SPB Git

spb/zyquo-agent Public MIT

The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.

Swift 94.7% Shell 4.1% Python 0.7% Makefile 0.5%
6.5 KB · 176 lines swift
Raw Blame History
1//2//  InputBarView.swift3//  Zyquo Agent4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Floating input card docked at the bottom of the conversation column:9//  auto-growing multiline editor, attach-text-file button (contents are10//  appended to the prompt), and the violet circular Run button (⌘↩) that11//  becomes Stop (⌘.) while a run is live. Disabled states explain themselves12//  (no model / no key).13//1415import SwiftUI16import UniformTypeIdentifiers1718struct InputBarView: View {19    @Binding var text: String20    let isRunning: Bool21    /// Non-nil disables Run and shows why (no key / no model).22    let disabledReason: String?23    var onRun: () -> Void24    var onStop: () -> Void2526    @State private var dropTargeted = false27    @EnvironmentObject private var appearance: AppearanceStore28    @FocusState private var editorFocused: Bool2930    private var canRun: Bool {31        disabledReason == nil && !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty32    }3334    var body: some View {35        VStack(spacing: ZyquoSpacing.xxs) {36            if let reason = disabledReason {37                HStack(spacing: ZyquoSpacing.xxs) {38                    Image(systemName: "exclamationmark.circle")39                        .font(.system(size: 10))40                    Text(reason)41                        .font(ZyquoFont.caption)42                }43                .foregroundStyle(ZyquoColor.warning)44                .frame(maxWidth: .infinity, alignment: .leading)45            }46            HStack(alignment: .bottom, spacing: ZyquoSpacing.xs) {47                attachButton48                editor49                runButton50            }51        }52        .padding(ZyquoSpacing.sm)53        .background(54            RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)55                .fill(ZyquoColor.surface)56                .overlay(57                    RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)58                        .strokeBorder(59                            dropTargeted ? ZyquoColor.accent : ZyquoColor.border,60                            style: StrokeStyle(61                                lineWidth: dropTargeted ? 1.5 : ZyquoMetrics.hairline,62                                dash: dropTargeted ? [6, 4] : []63                            )64                        )65                )66        )67        .zyquoSoftShadow()68        .padding(.horizontal, ZyquoSpacing.sm)69        .padding(.bottom, ZyquoSpacing.sm)70        .onDrop(of: [.fileURL], isTargeted: $dropTargeted) { providers in71            handleDrop(providers)72        }73    }7475    // MARK: - Pieces7677    private var editor: some View {78        ZStack(alignment: .topLeading) {79            if text.isEmpty {80                Text("What should I do on your Mac?")81                    .font(ZyquoFont.body(size: appearance.chatFontSize))82                    .foregroundStyle(ZyquoColor.textTertiary)83                    .padding(.top, 2)84                    .allowsHitTesting(false)85            }86            TextEditor(text: $text)87                .font(ZyquoFont.body(size: appearance.chatFontSize))88                .foregroundStyle(ZyquoColor.textPrimary)89                .scrollContentBackground(.hidden)90                .frame(minHeight: 22, maxHeight: 200)91                .fixedSize(horizontal: false, vertical: text.count < 2000)92                .focused($editorFocused)93                .onAppear { editorFocused = true }94        }95    }9697    private var attachButton: some View {98        Button {99            presentFilePicker()100        } label: {101            Image(systemName: "paperclip")102                .font(.system(size: 14))103                .foregroundStyle(ZyquoColor.textSecondary)104                .frame(width: 26, height: 26)105        }106        .buttonStyle(.plain)107        .zyquoHoverHighlight()108        .help("Attach text files (contents are added to the prompt)")109    }110111    private var runButton: some View {112        Button {113            isRunning ? onStop() : (canRun ? onRun() : ())114        } label: {115            Image(systemName: isRunning ? "stop.fill" : "arrow.up")116                .font(.system(size: 13, weight: .semibold))117                .foregroundStyle(.white)118                .frame(width: 28, height: 28)119                .background(120                    Circle().fill(121                        isRunning122                            ? ZyquoColor.danger123                            : (canRun ? ZyquoColor.accent : ZyquoColor.textTertiary)124                    )125                )126        }127        .buttonStyle(PressableButtonStyle())128        .keyboardShortcut(.return, modifiers: .command)129        .help(isRunning ? "Stop the run (⌘.)" : "Run (⌘↩)")130    }131132    // MARK: - Text-file intake133134    private static let textExtensions: Set<String> = [135        "txt", "md", "markdown", "csv", "json", "yaml", "yml", "xml", "log",136        "swift", "py", "js", "ts", "jsx", "tsx", "html", "css", "sh", "zsh",137        "bash", "sql", "go", "rs", "c", "h", "cpp", "hpp", "m", "mm", "java",138        "rb", "php", "toml", "ini", "cfg", "tex",139    ]140141    private func presentFilePicker() {142        let panel = NSOpenPanel()143        panel.allowsMultipleSelection = true144        panel.canChooseDirectories = false145        panel.begin { response in146            guard response == .OK else { return }147            for url in panel.urls { ingest(url: url) }148        }149    }150151    private func handleDrop(_ providers: [NSItemProvider]) -> Bool {152        var handled = false153        for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) {154            handled = true155            provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier) { item, _ in156                guard let data = item as? Data,157                      let url = URL(dataRepresentation: data, relativeTo: nil) else { return }158                DispatchQueue.main.async { ingest(url: url) }159            }160        }161        return handled162    }163164    /// Appends a readable text file's contents to the prompt, fenced and165    /// labeled with its name.166    private func ingest(url: URL) {167        let ext = url.pathExtension.lowercased()168        guard let data = try? Data(contentsOf: url),169              Self.textExtensions.contains(ext)170                || (String(data: data, encoding: .utf8) != nil && data.count < 512_000),171              let content = String(data: data, encoding: .utf8) else { return }172        let block = "\n\n--- \(url.lastPathComponent) ---\n\(content)\n--- end \(url.lastPathComponent) ---\n"173        text += block174    }175}176