Add usage commands, a vendor-neutral usage format and a knowledge base
This commit is contained in:
@@ -3,6 +3,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
typealias Permission = Components.Schemas.Permission
|
||||
typealias UsageCommands = Components.Schemas.UsageCommands
|
||||
|
||||
let serverHelper = Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre").path
|
||||
private let service = "gui/\(getuid())/run.fc.byre"
|
||||
@@ -215,6 +216,71 @@ struct PermissionsCheck: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct UsageCommandsForm: View {
|
||||
let api: API
|
||||
@ViewState private var saved: UsageCommands?
|
||||
@ViewState private var draft: UsageCommands?
|
||||
@ViewState private var error = ""
|
||||
@ViewState private var busy = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if draft != nil {
|
||||
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 6) {
|
||||
row("claude", \.claude)
|
||||
row("codex", \.codex)
|
||||
row("antigravity", \.antigravity)
|
||||
}
|
||||
Button(busy ? "Saving…" : "Save", action: save)
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.disabled(busy || draft == saved)
|
||||
}
|
||||
if !error.isEmpty {
|
||||
Text(error)
|
||||
.foregroundStyle(Palette.danger)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.task {
|
||||
do {
|
||||
saved = try await api.client.getUsageCommands().ok.body.json
|
||||
draft = saved
|
||||
error = ""
|
||||
} catch {
|
||||
self.error = "Couldn't load usage commands: \(errorText(error))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func row(_ vendor: String, _ key: WritableKeyPath<UsageCommands, String>) -> some View {
|
||||
GridRow {
|
||||
Text(vendor)
|
||||
TextField("Built in", text: Binding(get: { draft?[keyPath: key] ?? "" }, set: { draft?[keyPath: key] = $0 }))
|
||||
.modifier(FieldStyle())
|
||||
.onSubmit(save)
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
guard let draft, draft != saved, !busy else {
|
||||
return
|
||||
}
|
||||
busy = true
|
||||
Task {
|
||||
do {
|
||||
saved = try await api.client.updateUsageCommands(body: .json(draft)).ok.body.json
|
||||
self.draft = saved
|
||||
error = ""
|
||||
} catch {
|
||||
self.error = "Couldn't save usage commands: \(errorText(error))"
|
||||
}
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProcessFailure: LocalizedError {
|
||||
let errorDescription: String?
|
||||
}
|
||||
@@ -336,6 +402,13 @@ struct ServerView: View {
|
||||
PermissionsCheck {
|
||||
try await API(origin: origin).client.checkPermissions().ok.body.json
|
||||
}
|
||||
Text("Usage commands")
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.padding(.top, 16)
|
||||
Text("A shell command per vendor that prints its usage as JSON for the Status tab, run after each of that vendor's turns. Empty uses what the vendor itself reports.")
|
||||
.foregroundStyle(Palette.muted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
UsageCommandsForm(api: API(origin: origin))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
@@ -3,6 +3,8 @@ import SwiftUI
|
||||
|
||||
typealias AgentProcess = Components.Schemas.AgentProcess
|
||||
typealias Zombie = Components.Schemas.Zombie
|
||||
typealias VendorUsage = Components.Schemas.Usage
|
||||
typealias UsageWindow = Components.Schemas.UsageWindow
|
||||
|
||||
private let refreshKinds: Set<String> = ["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health", "admin.clients"]
|
||||
|
||||
@@ -33,8 +35,7 @@ private func bytes(_ n: Int64) -> String {
|
||||
return "\(v.formatted(.number.precision(.fractionLength(i == 0 ? 0 : 1)).grouping(.never))) \(units[i])"
|
||||
}
|
||||
|
||||
private func resetText(_ unixSeconds: Double) -> String {
|
||||
let at = Date(timeIntervalSince1970: unixSeconds)
|
||||
private func resetText(_ at: Date) -> String {
|
||||
let time = at.formatted(Date.FormatStyle().weekday(.abbreviated).hour().minute())
|
||||
let minutes = Int((at.timeIntervalSinceNow / 60).rounded())
|
||||
if minutes <= 0 {
|
||||
@@ -44,21 +45,38 @@ private func resetText(_ unixSeconds: Double) -> String {
|
||||
return "\(time) (in \(rel))"
|
||||
}
|
||||
|
||||
private func number(_ v: (any Sendable)??) -> Double? {
|
||||
switch v {
|
||||
case .some(.some(let x)):
|
||||
if let d = x as? Double {
|
||||
return d
|
||||
}
|
||||
if let i = x as? Int {
|
||||
return Double(i)
|
||||
}
|
||||
return nil
|
||||
private func amount(_ v: Double, _ unit: String) -> String {
|
||||
switch unit {
|
||||
case "usd":
|
||||
return v.formatted(.currency(code: "USD"))
|
||||
case "tokens":
|
||||
return v.formatted(.number.notation(.compactName).precision(.significantDigits(1...3)))
|
||||
default:
|
||||
return nil
|
||||
return v.formatted(.number.precision(.fractionLength(0...2)))
|
||||
}
|
||||
}
|
||||
|
||||
private func percentText(_ v: Double) -> String {
|
||||
"\(v.formatted(.number.precision(.fractionLength(0)).grouping(.never)))%"
|
||||
}
|
||||
|
||||
private func usedText(_ w: UsageWindow, _ fraction: Double?) -> String {
|
||||
if w.unit == "percent" {
|
||||
return percentText(w.used)
|
||||
}
|
||||
var s = amount(w.used, w.unit)
|
||||
if let limit = w.limit {
|
||||
s += " / \(amount(limit, w.unit))"
|
||||
}
|
||||
if w.unit != "usd" {
|
||||
s += " \(w.unit)"
|
||||
}
|
||||
if let fraction {
|
||||
s += " · \(percentText(fraction * 100))"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
struct StatusView: View {
|
||||
@Environment(Store.self) private var store
|
||||
@ViewState private var status: ServerStatus?
|
||||
@@ -79,7 +97,7 @@ struct StatusView: View {
|
||||
section("Running turns")
|
||||
TurnList(status: status)
|
||||
section("Rate limits")
|
||||
RateLimits(limits: status.rateLimits.additionalProperties.mapValues { $0.value })
|
||||
RateLimits(limits: status.rateLimits.additionalProperties)
|
||||
section("Clients")
|
||||
ClientTable(status: status)
|
||||
section("Server")
|
||||
@@ -263,11 +281,11 @@ private struct TurnList: View {
|
||||
}
|
||||
|
||||
private struct RateLimits: View {
|
||||
let limits: [String: JSONDict]
|
||||
let limits: [String: VendorUsage]
|
||||
|
||||
var body: some View {
|
||||
if limits.isEmpty {
|
||||
Text("No vendor has reported a rate-limit status since byre started.").foregroundStyle(Palette.muted)
|
||||
Text("No vendor has reported usage since byre started.").foregroundStyle(Palette.muted)
|
||||
} else {
|
||||
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 2) {
|
||||
GridRow {
|
||||
@@ -283,65 +301,51 @@ private struct RateLimits: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func vendorRows(_ vendor: String, _ info: JSONDict) -> some View {
|
||||
let statusText = info["status"].map { jsString($0) } ?? "unknown"
|
||||
let tone = statusText == "allowed" ? Palette.ok : statusText == "rejected" ? Palette.danger : Palette.warn
|
||||
let windows = dict(info, "unifiedWindows") ?? [:]
|
||||
let rest = info.filter { !["status", "rateLimitType", "resetsAt", "unifiedWindows"].contains($0.key) }.sorted { $0.key < $1.key }
|
||||
private func vendorRows(_ vendor: String, _ usage: VendorUsage) -> some View {
|
||||
let rejected = usage.rejected != nil || usage.windows.contains { w in w.limit.map { w.used >= $0 } ?? false }
|
||||
let reason = usage.rejected.flatMap { $0.isEmpty ? nil : $0 }
|
||||
GridRow {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text(vendor).font(Fonts.font(13, weight: 700)).foregroundStyle(Palette.text)
|
||||
Text(statusText).font(Fonts.font(11, weight: 700)).foregroundStyle(tone)
|
||||
Text(rejected ? "rejected\(reason.map { ": \($0)" } ?? "")" : "allowed").font(Fonts.font(11, weight: 700)).foregroundStyle(rejected ? Palette.danger : Palette.ok)
|
||||
Text("as of \(since(usage.updatedAt))").font(Fonts.font(11)).foregroundStyle(Palette.muted)
|
||||
}
|
||||
.padding(.top, 12)
|
||||
.gridCellColumns(5)
|
||||
}
|
||||
Divider().overlay(Palette.border)
|
||||
ForEach(windows.keys.sorted(), id: \.self) { name in
|
||||
let w = dict(windows, name) ?? [:]
|
||||
let used = number(w["utilization"])
|
||||
ForEach(Array(usage.windows.enumerated()), id: \.offset) { _, w in
|
||||
let used = w.limit.flatMap { $0 > 0 && (w.used / $0).isFinite ? w.used / $0 : nil }
|
||||
let elapsed = elapsedFraction(w)
|
||||
GridRow {
|
||||
Text(name)
|
||||
Text(used.map { "\(Int(($0 * 100).rounded()))%" } ?? "—")
|
||||
Text(w.label)
|
||||
Text(usedText(w, used))
|
||||
.foregroundStyle(used.map { $0 >= 0.9 ? Palette.danger : $0 >= 0.7 ? Palette.warn : Palette.text } ?? Palette.text)
|
||||
Text(elapsed.map { "\(Int(($0 * 100).rounded()))%" } ?? "—")
|
||||
Text(elapsed.map { percentText($0 * 100) } ?? "—")
|
||||
PaceBar(used: used, elapsed: elapsed)
|
||||
Text(number(w["resetsAt"]).map(resetText) ?? "—")
|
||||
Text(w.resetsAt.map(resetText) ?? "—")
|
||||
}
|
||||
Divider().overlay(Palette.border)
|
||||
}
|
||||
ForEach(rest, id: \.key) { k, v in
|
||||
GridRow {
|
||||
Text(k).foregroundStyle(Palette.muted)
|
||||
Text(v is JSONDict || v is [(any Sendable)?] ? compactJSONText(v) : jsString(v))
|
||||
.textSelection(.enabled)
|
||||
.gridCellColumns(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func elapsedFraction(_ w: JSONDict) -> Double? {
|
||||
guard let resets = number(w["resetsAt"]), let mins = number(w["windowMins"]) else {
|
||||
private func elapsedFraction(_ w: UsageWindow) -> Double? {
|
||||
guard let resets = w.resetsAt, let seconds = w.windowSeconds, seconds > 0 else {
|
||||
return nil
|
||||
}
|
||||
let remaining = resets * 1000 - Date().timeIntervalSince1970 * 1000
|
||||
return min(1, max(0, 1 - remaining / (mins * 60000)))
|
||||
return min(1, max(0, 1 - resets.timeIntervalSinceNow / Double(seconds)))
|
||||
}
|
||||
}
|
||||
|
||||
private func compactJSONText(_ v: (any Sendable)?) -> String {
|
||||
prettyJSON(v).components(separatedBy: "\n").map { trimmed($0) }.joined(separator: " ")
|
||||
}
|
||||
|
||||
private struct PaceBar: View {
|
||||
let used: Double?
|
||||
let elapsed: Double?
|
||||
|
||||
var body: some View {
|
||||
if let used, let elapsed {
|
||||
let diff = Int(((used - elapsed) * 100).rounded())
|
||||
let title = diff > 0 ? "\(diff) points ahead of an even pace" : diff < 0 ? "\(-diff) points behind an even pace" : "on an even pace"
|
||||
let diff = ((used - elapsed) * 100).rounded()
|
||||
let points = abs(diff).formatted(.number.precision(.fractionLength(0)).grouping(.never))
|
||||
let title = diff > 0 ? "\(points) points ahead of an even pace" : diff < 0 ? "\(points) points behind an even pace" : "on an even pace"
|
||||
GeometryReader { g in
|
||||
ZStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
|
||||
Reference in New Issue
Block a user