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%
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