SPB Git

spb/zyquo-router Public MIT

One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).

Swift 95.7% Python 2.3% Shell 1.2% Makefile 0.9%
6.0 KB · 166 lines swift
Raw Blame History
1//2//  CommandPalette.swift3//  Zyquo Router4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  ⌘K: fuzzy access to everything — start/stop, sections, copy endpoint,9//  and copy any model ID. Return runs the highlighted action.10//1112import SwiftUI1314struct CommandPalette: View {15    @Binding var isPresented: Bool16    @EnvironmentObject private var server: ServerController17    @EnvironmentObject private var catalog: ModelCatalog1819    @State private var query = ""20    @State private var highlighted = 021    @FocusState private var fieldFocused: Bool2223    private struct Action: Identifiable {24        let id: String25        let title: String26        let subtitle: String?27        let systemImage: String28        let run: () -> Void29    }3031    private var actions: [Action] {32        var all: [Action] = [33            Action(34                id: "server",35                title: server.isRunning ? "Stop Server" : "Start Server",36                subtitle: server.isRunning ? "Port \(server.port)" : nil,37                systemImage: "power",38                run: { server.toggle() }39            ),40            Action(41                id: "copy-endpoint",42                title: "Copy Endpoint URL",43                subtitle: server.endpointURL,44                systemImage: "doc.on.doc",45                run: {46                    NSPasteboard.general.clearContents()47                    NSPasteboard.general.setString(server.endpointURL, forType: .string)48                }49            ),50        ]51        for section in AppSection.allCases {52            all.append(Action(53                id: "go-\(section.rawValue)",54                title: "Go to \(section.rawValue)",55                subtitle: nil,56                systemImage: section.systemImage,57                run: { UserDefaults.standard.set(section.rawValue, forKey: "selectedSection") }58            ))59        }60        for model in catalog.all {61            let id = RequestRouter.namespacedID(for: model)62            all.append(Action(63                id: "model-\(id)",64                title: id,65                subtitle: "Copy model ID",66                systemImage: "square.grid.2x2",67                run: {68                    NSPasteboard.general.clearContents()69                    NSPasteboard.general.setString(id, forType: .string)70                }71            ))72        }73        return all74    }7576    private var filtered: [Action] {77        let trimmed = query.trimmingCharacters(in: .whitespaces)78        guard !trimmed.isEmpty else {79            return Array(actions.prefix(9))80        }81        return Array(actions.filter {82            $0.title.localizedCaseInsensitiveContains(trimmed)83        }.prefix(9))84    }8586    var body: some View {87        VStack(spacing: 0) {88            TextField("Type a command or model…", text: $query)89                .textFieldStyle(.plain)90                .font(ZyquoFont.body(size: 15))91                .padding(ZyquoSpacing.md)92                .focused($fieldFocused)93                .onSubmit { runHighlighted() }94                .onChange(of: query) { _ in highlighted = 0 }9596            ZyquoHairline()9798            VStack(spacing: 0) {99                ForEach(Array(filtered.enumerated()), id: \.element.id) { index, action in100                    HStack(spacing: ZyquoSpacing.sm) {101                        Image(systemName: action.systemImage)102                            .font(.system(size: 12))103                            .foregroundStyle(index == highlighted ? ZyquoColor.accent : ZyquoColor.textSecondary)104                            .frame(width: 18)105                        Text(action.title)106                            .font(action.id.hasPrefix("model-") ? ZyquoFont.mono(size: 12.5) : ZyquoFont.body())107                            .foregroundStyle(ZyquoColor.textPrimary)108                            .lineLimit(1)109                            .truncationMode(.middle)110                        Spacer()111                        if let subtitle = action.subtitle {112                            Text(subtitle)113                                .font(ZyquoFont.caption)114                                .foregroundStyle(ZyquoColor.textTertiary)115                                .lineLimit(1)116                        }117                    }118                    .padding(.horizontal, ZyquoSpacing.md)119                    .padding(.vertical, ZyquoSpacing.xs)120                    .background(index == highlighted ? ZyquoColor.accentSubtle : .clear)121                    .contentShape(Rectangle())122                    .onTapGesture {123                        action.run()124                        isPresented = false125                    }126                    .onHover { hover in127                        if hover { highlighted = index }128                    }129                }130            }131            .padding(.vertical, ZyquoSpacing.xxs)132        }133        .frame(width: 520)134        .background(135            RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)136                .fill(ZyquoColor.surface)137                .overlay(138                    RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)139                        .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)140                )141        )142        .zyquoSoftShadow()143        .onAppear { fieldFocused = true }144        .background(paletteKeyHandlers)145    }146147    /// Hidden buttons carrying the arrow-key/escape shortcuts.148    private var paletteKeyHandlers: some View {149        Group {150            Button("") { highlighted = min(highlighted + 1, filtered.count - 1) }151                .keyboardShortcut(.downArrow, modifiers: [])152            Button("") { highlighted = max(highlighted - 1, 0) }153                .keyboardShortcut(.upArrow, modifiers: [])154            Button("") { isPresented = false }155                .keyboardShortcut(.escape, modifiers: [])156        }157        .hidden()158    }159160    private func runHighlighted() {161        guard filtered.indices.contains(highlighted) else { return }162        filtered[highlighted].run()163        isPresented = false164    }165}166