Start a workflow from the iPhone Action button
This commit is contained in:
@@ -140,6 +140,10 @@ struct PhoneView: View {
|
||||
.environment(overlays)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: Binding(get: { QuickRunRequest.shared.shown }, set: { QuickRunRequest.shared.shown = $0 })) {
|
||||
QuickRunPage()
|
||||
.environment(store)
|
||||
}
|
||||
.onAppear {
|
||||
store.paneFocus.attach()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
struct QuickRunPage: View {
|
||||
@Environment(Store.self) private var store
|
||||
@ViewState private var text = ""
|
||||
@ViewState private var starting = false
|
||||
@ViewState private var failure: String?
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
var body: some View {
|
||||
let q = store.me?.quickRun ?? .init(projectId: "", workflow: "")
|
||||
let project = store.projects.first { $0.id == q.projectId }
|
||||
let prompt = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
close()
|
||||
}
|
||||
.disabled(starting)
|
||||
Spacer()
|
||||
if !q.workflow.isEmpty {
|
||||
Button("Start") {
|
||||
start(q, prompt)
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(primary: true))
|
||||
.disabled(prompt.isEmpty || starting)
|
||||
}
|
||||
}
|
||||
.font(Fonts.font(15, weight: 700))
|
||||
if q.workflow.isEmpty {
|
||||
Text("Choose the workflow the Action button starts in More → Settings.")
|
||||
.foregroundStyle(Palette.muted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(q.workflow)
|
||||
.font(Fonts.font(17, weight: 700))
|
||||
Text(project?.name ?? q.projectId)
|
||||
.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(q, prompt)
|
||||
}
|
||||
}
|
||||
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))
|
||||
.onAppear {
|
||||
focused = true
|
||||
}
|
||||
}
|
||||
|
||||
private func start(_ q: Components.Schemas.QuickRun, _ prompt: String) {
|
||||
if prompt.isEmpty || starting {
|
||||
return
|
||||
}
|
||||
starting = true
|
||||
failure = nil
|
||||
Task {
|
||||
do {
|
||||
let task = try await store.api.client.createTask(path: .init(projectId: q.projectId), body: .json(.init(prompt: prompt, workflow: q.workflow))).created.body.json
|
||||
close()
|
||||
store.focusTask(task.id)
|
||||
} catch {
|
||||
starting = false
|
||||
failure = errorText(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func close() {
|
||||
text = ""
|
||||
starting = false
|
||||
failure = nil
|
||||
QuickRunRequest.shared.shown = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -54,6 +54,11 @@ struct SettingsView: View {
|
||||
ModelMenu(current: store.defaultModel, help: "Model for new tasks") { m in
|
||||
try await store.api.client.updateDefaultModel(body: .json(.init(model: m))).ok.body.json
|
||||
}
|
||||
heading("Action button workflow")
|
||||
Text("The iPhone's Action button, set to byre's Run workflow shortcut, opens a page to type a prompt and start this workflow with it.")
|
||||
.foregroundStyle(Palette.muted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
QuickRunSetting()
|
||||
heading("Saved commands")
|
||||
Text("Each is a button in the Terminal tab of every task on this server. It opens a new shell in the task's worktree and runs the command there.")
|
||||
.foregroundStyle(Palette.muted)
|
||||
|
||||
+6
-1
@@ -5,10 +5,12 @@ root=$(dirname "$mac")
|
||||
shared=$HOME/Library/Caches/byre/build
|
||||
sdk=iphoneos
|
||||
destination="generic/platform=iOS"
|
||||
triple=arm64-apple-ios27.0
|
||||
link=
|
||||
if [ "${1:-}" = --simulator ]; then
|
||||
sdk=iphonesimulator
|
||||
destination="generic/platform=iOS Simulator"
|
||||
triple=arm64-apple-ios27.0-simulator
|
||||
link="-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __entitlements -Xlinker $shared/mac/phone/byre.entitlements"
|
||||
fi
|
||||
build=$mac/.build/phone/$sdk
|
||||
@@ -25,7 +27,10 @@ rm -rf "$5"
|
||||
mkdir -p "$5/Frameworks"
|
||||
cp "$products/Byre" "$5/Byre"
|
||||
ditto "$products/TailscaleKit.framework" "$5/Frameworks/TailscaleKit.framework"
|
||||
' sh "$root" "$shared" "$sdk" "$destination" "$app" "$link"
|
||||
objects=$2/phone/Build/Intermediates.noindex/Byre.build/Release-$3/Byre-p.build
|
||||
echo "$objects/Objects-normal/arm64/Byre-primary.swiftconstvalues" > "$objects/Byre.SwiftConstValuesFileList"
|
||||
xcrun appintentsmetadataprocessor --output "$5" --toolchain-dir "$(dirname "$(dirname "$(dirname "$(xcrun --find swiftc)")")")" --module-name Byre --sdk-root "$(xcrun --sdk "$3" --show-sdk-path)" --xcode-version "$(xcodebuild -version | awk "NR == 2 { print \$3 }")" --platform-family iOS --deployment-target 27.0 --target-triple "$7" --source-file-list "$objects/Objects-normal/arm64/Byre.SwiftFileList" --swift-const-vals-list "$objects/Byre.SwiftConstValuesFileList" --metadata-file-list "$objects/Byre.DependencyMetadataFileList" --static-metadata-file-list "$objects/Byre.DependencyStaticMetadataFileList" --binary-file "$5/Byre" --no-app-shortcuts-localization
|
||||
' sh "$root" "$shared" "$sdk" "$destination" "$app" "$link" "$triple"
|
||||
assets=$build/Assets.xcassets/AppIcon.appiconset
|
||||
rm -rf "$build/Assets.xcassets"
|
||||
mkdir -p "$assets"
|
||||
|
||||
Reference in New Issue
Block a user