Files
byre/mac/Sources/Byre/QuickRun.swift
T

265 lines
9.0 KiB
Swift
Raw Normal View History

import ByreAPI
import SwiftUI
struct QuickRunSetting: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var workflows: [WorkflowSummary] = []
@ViewState private var off: (() -> Void)?
var body: some View {
let current = store.me?.quickRun ?? .init(projectId: "", workflow: "")
HStack(spacing: 6) {
menu(current.workflow.isEmpty ? "None" : current.workflow, help: "Workflow the Action button starts") {
Button("None") {
save(.init(projectId: "", workflow: ""))
}
ForEach(workflows, id: \.name) { w in
Button(w.name) {
save(.init(projectId: current.projectId.isEmpty ? store.projects.first?.id ?? "" : current.projectId, workflow: w.name))
}
}
}
if !current.workflow.isEmpty {
Text("in")
.foregroundStyle(Palette.muted)
menu(store.projects.first { $0.id == current.projectId }?.name ?? current.projectId, help: "Project the workflow runs in") {
ForEach(store.projects, id: \.id) { p in
Button(p.name) {
save(.init(projectId: p.id, workflow: current.workflow))
}
}
}
}
}
.padding(.top, 8)
.onAppear {
off = store.events.add { ev in
if ev.kind == "workflow.updated" || ev.kind == "workflow.deleted" {
load()
}
}
load()
}
.onDisappear {
off?()
off = nil
}
}
private func menu(_ label: String, help: String, @ViewBuilder _ items: () -> some View) -> some View {
Menu {
items()
} label: {
HStack(spacing: 8) {
Text(label)
Image(systemName: "chevron.up.chevron.down")
.font(Fonts.font(9, weight: 600))
}
.foregroundStyle(Palette.text)
.modifier(FieldStyle())
.contentShape(Rectangle())
}
.menuStyle(.button)
.buttonStyle(.plain)
.menuIndicator(.hidden)
.fixedSize()
.help(help)
}
private func load() {
Task {
do {
workflows = try await store.api.client.listWorkflows().ok.body.json
} catch {
overlays.showAlert(errorText(error))
}
}
}
private func save(_ q: Components.Schemas.QuickRun) {
Task {
do {
store.me = try await store.api.client.updateQuickRun(body: .json(q)).ok.body.json
} catch {
overlays.showAlert(errorText(error))
}
}
}
}
#if os(iOS)
import AppIntents
@MainActor
@Observable
final class QuickRunRequest {
static let shared = QuickRunRequest()
var shown = false
}
struct QuickRunIntent: AppIntent {
static let title: LocalizedStringResource = "Run byre workflow"
static let description = IntentDescription("Opens byre to type a prompt and start your Action button workflow with it.")
static let supportedModes: IntentModes = .foreground
@MainActor
func perform() async throws -> some IntentResult {
var t = Transaction()
t.disablesAnimations = true
withTransaction(t) {
QuickRunRequest.shared.shown = true
}
return .result()
}
}
struct ByreShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(intent: QuickRunIntent(), phrases: ["Run a workflow in \(.applicationName)"], shortTitle: "Run workflow", systemImageName: "bolt")
}
}
@MainActor
private struct QuickRunChoice {
let origin: String
let store: Store
let overlays: Overlays
let run: Components.Schemas.QuickRun
var project: String {
store.projects.first { $0.id == run.projectId }?.name ?? run.projectId
}
}
struct QuickRunPage: View {
@ViewState private var picked: String?
@ViewState private var text = ""
@ViewState private var starting = false
@ViewState private var failure: String?
@FocusState private var focused: Bool
private var choices: [QuickRunChoice] {
Servers.shared.origins.compactMap { origin in
guard let connection = Servers.shared.connections[origin], let store = connection.store, let run = store.me?.quickRun, !run.workflow.isEmpty else {
return nil
}
return QuickRunChoice(origin: origin, store: store, overlays: connection.overlays, run: run)
}
}
private var connecting: Bool {
Servers.shared.connections.values.contains {
if case .starting = $0.phase {
return true
}
return false
}
}
var body: some View {
let choices = choices
let choice = choices.first { $0.origin == picked } ?? (choices.count == 1 ? choices.first : nil)
let prompt = text.trimmingCharacters(in: .whitespacesAndNewlines)
VStack(alignment: .leading, spacing: 12) {
HStack {
Button("Cancel") {
close()
}
.disabled(starting)
Spacer()
if let choice {
Button("Start") {
start(choice, prompt)
}
.buttonStyle(WebButtonStyle(primary: true))
.disabled(prompt.isEmpty || starting)
}
}
.font(Fonts.font(15, weight: 700))
if let choice {
VStack(alignment: .leading, spacing: 2) {
Text(choice.run.workflow)
.font(Fonts.font(17, weight: 700))
Text("\(choice.project) · \(serverName(choice.origin))")
.foregroundStyle(Palette.muted)
}
AgentEditor(text: $text, placeholder: "What should it do?", key: "quick-run", height: 220, focus: $focused, border: focused ? Palette.accent : Palette.border) {
start(choice, prompt)
}
.environment(choice.store)
.environment(choice.overlays)
.onAppear {
focused = true
}
} else if !choices.isEmpty {
ForEach(choices, id: \.origin) { c in
Button {
picked = c.origin
} label: {
VStack(alignment: .leading, spacing: 2) {
Text(c.run.workflow)
.font(Fonts.font(17, weight: 700))
Text("\(c.project) · \(serverName(c.origin))")
.foregroundStyle(Palette.muted)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Servers.shared.color(c.origin)))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
} else if connecting {
Text("Connecting to your servers…")
.foregroundStyle(Palette.muted)
} else {
Text("Choose the workflow the Action button starts in More → Settings.")
.foregroundStyle(Palette.muted)
.fixedSize(horizontal: false, vertical: true)
}
if let failure {
Text(failure)
.foregroundStyle(Palette.danger)
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 0)
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.foregroundStyle(Palette.text)
.tint(Palette.accent)
.background(Palette.bg)
.font(Fonts.font(13))
}
private func start(_ choice: QuickRunChoice, _ prompt: String) {
if prompt.isEmpty || starting {
return
}
starting = true
failure = nil
Task {
do {
let task = try await choice.store.api.client.createTask(path: .init(projectId: choice.run.projectId), body: .json(.init(prompt: prompt, workflow: choice.run.workflow))).created.body.json
close()
Servers.shared.current = choice.origin
choice.store.focusTask(task.id)
} catch {
starting = false
failure = errorText(error)
}
}
}
private func close() {
picked = nil
text = ""
starting = false
failure = nil
QuickRunRequest.shared.shown = false
}
}
#endif