Fix overflow, diagram zoom, task-switch stall and other QA findings
This commit is contained in:
@@ -125,7 +125,7 @@ final class API: Sendable {
|
||||
"Byre-Client": clientID,
|
||||
"Byre-Commit": Bundle.main.object(forInfoDictionaryKey: "ByreCommit") as! String,
|
||||
"Byre-Device": deviceKind,
|
||||
"Byre-Host": deviceName,
|
||||
"Byre-Host": deviceName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!,
|
||||
]
|
||||
config.proxyConfigurations = [tailnetNode.proxy()]
|
||||
session = URLSession(configuration: config, delegate: RequestTiming(), delegateQueue: nil)
|
||||
|
||||
@@ -1346,11 +1346,14 @@ func fill(_ b: PlacedBox) -> Color {
|
||||
struct BoxCanvas: View {
|
||||
let layout: BoxLayout
|
||||
var focus: BoxFocus?
|
||||
var region: CGRect?
|
||||
|
||||
var body: some View {
|
||||
Canvas { context, size in
|
||||
let scale = layout.size.width > 0 ? size.width / layout.size.width : 1
|
||||
let r = region ?? CGRect(origin: .zero, size: layout.size)
|
||||
let scale = r.width > 0 ? size.width / r.width : 1
|
||||
context.scaleBy(x: scale, y: scale)
|
||||
context.translateBy(x: -r.minX, y: -r.minY)
|
||||
draw(&context)
|
||||
}
|
||||
}
|
||||
@@ -1565,17 +1568,43 @@ struct DiagramViewer: View {
|
||||
}
|
||||
}
|
||||
|
||||
private let minZoom: CGFloat = 0.25
|
||||
private let maxZoom: CGFloat = 6
|
||||
|
||||
private struct SizedCanvas: View {
|
||||
let layout: BoxLayout
|
||||
let focus: BoxFocus?
|
||||
let zoom: CGFloat
|
||||
|
||||
var body: some View {
|
||||
BoxCanvas(layout: layout, focus: focus)
|
||||
.frame(width: layout.size.width, height: layout.size.height)
|
||||
.diagramMenu(layout)
|
||||
ZStack(alignment: .topLeading) {
|
||||
ForEach(Array(tiles(layout.size, side: 1024 / zoom).enumerated()), id: \.offset) { _, r in
|
||||
BoxCanvas(layout: layout, focus: focus, region: r)
|
||||
.frame(width: r.width * zoom, height: r.height * zoom)
|
||||
.offset(x: r.minX * zoom, y: r.minY * zoom)
|
||||
}
|
||||
}
|
||||
.frame(width: layout.size.width * zoom, height: layout.size.height * zoom, alignment: .topLeading)
|
||||
.diagramMenu(layout)
|
||||
}
|
||||
}
|
||||
|
||||
// The zoom views draw at their zoom rather than scaling a natural-size view, since SwiftUI then draws only a window's
|
||||
// width of it; tiles keep each canvas within the largest texture the GPU takes.
|
||||
private func tiles(_ size: CGSize, side: CGFloat) -> [CGRect] {
|
||||
var out: [CGRect] = []
|
||||
var y: CGFloat = 0
|
||||
while y < size.height {
|
||||
var x: CGFloat = 0
|
||||
while x < size.width {
|
||||
out.append(CGRect(x: x, y: y, width: min(side, size.width - x), height: min(side, size.height - y)))
|
||||
x += side
|
||||
}
|
||||
y += side
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private func click(_ layout: BoxLayout, _ p: CGPoint, tap: (String?) -> Void, open: (String) -> Void) {
|
||||
if let id = layout.opener(at: p) {
|
||||
open(id)
|
||||
@@ -1614,15 +1643,64 @@ private final class CenteringClipView: NSClipView {
|
||||
}
|
||||
|
||||
private final class FittingScrollView: NSScrollView {
|
||||
var fitted = false
|
||||
let host: NSHostingView<SizedCanvas>
|
||||
var diagram: BoxLayout
|
||||
var focus: BoxFocus?
|
||||
private(set) var zoom: CGFloat = 1
|
||||
private var fit: CGFloat = 1
|
||||
private var fitted: CGSize?
|
||||
|
||||
init(_ diagram: BoxLayout, focus: BoxFocus?) {
|
||||
self.diagram = diagram
|
||||
self.focus = focus
|
||||
host = NSHostingView(rootView: SizedCanvas(layout: diagram, focus: focus, zoom: 1))
|
||||
super.init(frame: .zero)
|
||||
contentView = CenteringClipView()
|
||||
drawsBackground = false
|
||||
hasVerticalScroller = true
|
||||
hasHorizontalScroller = true
|
||||
autohidesScrollers = true
|
||||
allowsMagnification = true
|
||||
documentView = host
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(endMagnify), name: NSScrollView.didEndLiveMagnifyNotification, object: self)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
func render() {
|
||||
host.rootView = SizedCanvas(layout: diagram, focus: focus, zoom: zoom)
|
||||
host.frame = CGRect(x: 0, y: 0, width: diagram.size.width * zoom, height: diagram.size.height * zoom)
|
||||
minMagnification = min(minZoom, fit) / zoom
|
||||
maxMagnification = maxZoom / zoom
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
guard !fitted, let doc = documentView, bounds.width > 0, bounds.height > 0, doc.frame.width > 0 else {
|
||||
guard fitted != bounds.size, bounds.width > 0, bounds.height > 0, diagram.size.width > 0, diagram.size.height > 0 else {
|
||||
return
|
||||
}
|
||||
fitted = true
|
||||
magnification = min(2, bounds.width / doc.frame.width, bounds.height / doc.frame.height)
|
||||
fitted = bounds.size
|
||||
fit = min(2, bounds.width / diagram.size.width, bounds.height / diagram.size.height)
|
||||
zoom = fit
|
||||
magnification = 1
|
||||
render()
|
||||
}
|
||||
|
||||
@objc private func endMagnify() {
|
||||
let m = magnification
|
||||
guard m != 1 else {
|
||||
return
|
||||
}
|
||||
let visible = contentView.bounds
|
||||
let center = CGPoint(x: visible.midX * m, y: visible.midY * m)
|
||||
zoom *= m
|
||||
magnification = 1
|
||||
render()
|
||||
let size = contentView.bounds.size
|
||||
contentView.scroll(to: CGPoint(x: center.x - size.width / 2, y: center.y - size.height / 2))
|
||||
reflectScrolledClipView(contentView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1635,16 +1713,18 @@ private struct ZoomingCanvas: NSViewRepresentable {
|
||||
@MainActor
|
||||
final class Coordinator: NSObject {
|
||||
var canvas: ZoomingCanvas
|
||||
weak var scroll: FittingScrollView?
|
||||
|
||||
init(_ canvas: ZoomingCanvas) {
|
||||
self.canvas = canvas
|
||||
}
|
||||
|
||||
@objc func clicked(_ g: NSClickGestureRecognizer) {
|
||||
guard let view = g.view else {
|
||||
guard let view = g.view, let zoom = scroll?.zoom else {
|
||||
return
|
||||
}
|
||||
click(canvas.layout, g.location(in: view), tap: canvas.tap, open: canvas.open)
|
||||
let p = g.location(in: view)
|
||||
click(canvas.layout, CGPoint(x: p.x / zoom, y: p.y / zoom), tap: canvas.tap, open: canvas.open)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1653,68 +1733,74 @@ private struct ZoomingCanvas: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> FittingScrollView {
|
||||
let scroll = FittingScrollView()
|
||||
scroll.contentView = CenteringClipView()
|
||||
scroll.drawsBackground = false
|
||||
scroll.hasVerticalScroller = true
|
||||
scroll.hasHorizontalScroller = true
|
||||
scroll.autohidesScrollers = true
|
||||
scroll.allowsMagnification = true
|
||||
scroll.minMagnification = 0.25
|
||||
scroll.maxMagnification = 6
|
||||
let host = NSHostingView(rootView: SizedCanvas(layout: layout, focus: focus))
|
||||
host.frame = CGRect(origin: .zero, size: layout.size)
|
||||
host.addGestureRecognizer(NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.clicked)))
|
||||
scroll.documentView = host
|
||||
let scroll = FittingScrollView(layout, focus: focus)
|
||||
scroll.host.addGestureRecognizer(NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.clicked)))
|
||||
context.coordinator.scroll = scroll
|
||||
return scroll
|
||||
}
|
||||
|
||||
func updateNSView(_ scroll: FittingScrollView, context: Context) {
|
||||
context.coordinator.canvas = self
|
||||
(scroll.documentView as? NSHostingView<SizedCanvas>)?.rootView = SizedCanvas(layout: layout, focus: focus)
|
||||
scroll.diagram = layout
|
||||
scroll.focus = focus
|
||||
scroll.render()
|
||||
}
|
||||
}
|
||||
#else
|
||||
private struct TappableCanvas: View {
|
||||
let layout: BoxLayout
|
||||
let focus: BoxFocus?
|
||||
let zoom: CGFloat
|
||||
let tap: (String?) -> Void
|
||||
let open: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
SizedCanvas(layout: layout, focus: focus)
|
||||
SizedCanvas(layout: layout, focus: focus, zoom: zoom)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(coordinateSpace: .local) { p in
|
||||
click(layout, p, tap: tap, open: open)
|
||||
click(layout, CGPoint(x: p.x / zoom, y: p.y / zoom), tap: tap, open: open)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class FittingScrollView: UIScrollView, UIScrollViewDelegate {
|
||||
var canvas: TappableCanvas
|
||||
let host: UIHostingController<TappableCanvas>
|
||||
private var fitted = false
|
||||
private var zoom: CGFloat = 1
|
||||
private var fit: CGFloat = 1
|
||||
private var fitted: CGSize?
|
||||
|
||||
init(_ canvas: TappableCanvas, size: CGSize) {
|
||||
init(_ canvas: TappableCanvas) {
|
||||
self.canvas = canvas
|
||||
host = UIHostingController(rootView: canvas)
|
||||
super.init(frame: .zero)
|
||||
host.view.backgroundColor = .clear
|
||||
host.view.frame = CGRect(origin: .zero, size: size)
|
||||
addSubview(host.view)
|
||||
contentSize = size
|
||||
delegate = self
|
||||
minimumZoomScale = 0.25
|
||||
maximumZoomScale = 6
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
func render() {
|
||||
let size = canvas.layout.size
|
||||
host.rootView = TappableCanvas(layout: canvas.layout, focus: canvas.focus, zoom: zoom, tap: canvas.tap, open: canvas.open)
|
||||
host.view.frame = CGRect(x: 0, y: 0, width: size.width * zoom, height: size.height * zoom)
|
||||
contentSize = host.view.frame.size
|
||||
minimumZoomScale = min(minZoom, fit) / zoom
|
||||
maximumZoomScale = maxZoom / zoom
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if !fitted, bounds.width > 0, bounds.height > 0, host.view.bounds.width > 0 {
|
||||
fitted = true
|
||||
zoomScale = min(2, bounds.width / host.view.bounds.width, bounds.height / host.view.bounds.height)
|
||||
let size = canvas.layout.size
|
||||
if fitted != bounds.size, bounds.width > 0, bounds.height > 0, size.width > 0, size.height > 0 {
|
||||
fitted = bounds.size
|
||||
fit = min(2, bounds.width / size.width, bounds.height / size.height)
|
||||
zoom = fit
|
||||
zoomScale = 1
|
||||
render()
|
||||
}
|
||||
center()
|
||||
}
|
||||
@@ -1727,6 +1813,18 @@ private final class FittingScrollView: UIScrollView, UIScrollViewDelegate {
|
||||
center()
|
||||
}
|
||||
|
||||
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||
guard scale != 1 else {
|
||||
return
|
||||
}
|
||||
let offset = contentOffset
|
||||
zoom *= scale
|
||||
zoomScale = 1
|
||||
render()
|
||||
contentOffset = offset
|
||||
center()
|
||||
}
|
||||
|
||||
private func center() {
|
||||
let x = max(0, (bounds.width - contentSize.width) / 2)
|
||||
let y = max(0, (bounds.height - contentSize.height) / 2)
|
||||
@@ -1741,11 +1839,12 @@ private struct ZoomingCanvas: UIViewRepresentable {
|
||||
let open: (String) -> Void
|
||||
|
||||
func makeUIView(context: Context) -> FittingScrollView {
|
||||
FittingScrollView(TappableCanvas(layout: layout, focus: focus, tap: tap, open: open), size: layout.size)
|
||||
FittingScrollView(TappableCanvas(layout: layout, focus: focus, zoom: 1, tap: tap, open: open))
|
||||
}
|
||||
|
||||
func updateUIView(_ scroll: FittingScrollView, context: Context) {
|
||||
scroll.host.rootView = TappableCanvas(layout: layout, focus: focus, tap: tap, open: open)
|
||||
scroll.canvas = TappableCanvas(layout: layout, focus: focus, zoom: 1, tap: tap, open: open)
|
||||
scroll.render()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -457,7 +457,7 @@ final class ChatModel {
|
||||
if let q = groups.first(where: { $0.id == "questions" }) {
|
||||
return flagged(q)
|
||||
}
|
||||
if let busy = store.progress(task) {
|
||||
if let busy = task.state == .running ? store.taskState(task) : store.progress(task) {
|
||||
return ChatStatus(label: busy.label, help: "\(busy.title)\(end)", tone: .working, group: nil)
|
||||
}
|
||||
return groups.first.map(flagged)
|
||||
@@ -991,7 +991,7 @@ private struct TranscriptScroll: View {
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
if running && model.key != nil {
|
||||
if running && !model.paused && model.key != nil {
|
||||
Scanner(color: accent)
|
||||
.frame(width: 84, height: 2)
|
||||
.offset(y: 8)
|
||||
@@ -1303,8 +1303,10 @@ struct WhenText: View {
|
||||
let when: Date
|
||||
|
||||
var body: some View {
|
||||
Text(ago(when))
|
||||
.help(isoText(when))
|
||||
TimelineView(.periodic(from: when, by: 60)) { _ in
|
||||
Text(ago(when))
|
||||
}
|
||||
.help(isoText(when))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -659,12 +659,31 @@ struct ComposerView: View {
|
||||
chat.showStatus(s)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
controls(m)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
status
|
||||
actions(live?.id, running: sessionState == .running, paused: live?.paused ?? false)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
let buttons = actions(live?.id, running: sessionState == .running, paused: live?.paused ?? false)
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 8) {
|
||||
controls(m)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
status
|
||||
buttons
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
VStack(alignment: .trailing, spacing: 8) {
|
||||
status
|
||||
HStack(spacing: 8) {
|
||||
controls(m)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
buttons
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
controls(m)
|
||||
HStack(spacing: 8) {
|
||||
Spacer(minLength: 0)
|
||||
status
|
||||
buttons
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
@@ -137,9 +137,6 @@ final class Overlays {
|
||||
if switcher != nil {
|
||||
return
|
||||
}
|
||||
#if os(macOS)
|
||||
logFocus("open switcher")
|
||||
#endif
|
||||
switcher = SwitcherModel(store: store, overlays: self, anchor: pickerFrame)
|
||||
}
|
||||
|
||||
@@ -293,16 +290,6 @@ final class Overlays {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@MainActor
|
||||
func logFocus(_ what: String) {
|
||||
let key = NSApp.keyWindow
|
||||
let main = NSApp.mainWindow
|
||||
let responder = key?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
log.notice("focus debug: \(what, privacy: .public) active=\(NSApp.isActive) key=\(key?.title ?? "nil", privacy: .public) main=\(main?.title ?? "nil", privacy: .public) mainIsKey=\(main?.isKeyWindow ?? false) responder=\(responder, privacy: .public)")
|
||||
}
|
||||
#endif
|
||||
|
||||
struct ModalText: View {
|
||||
let text: String
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ final class NetworkWatch {
|
||||
static let shared = NetworkWatch()
|
||||
|
||||
private let monitor = NWPathMonitor()
|
||||
private var seen = false
|
||||
private var last: String?
|
||||
|
||||
private init() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
let summary = "\(path.status) \(path.availableInterfaces.map(\.name).joined(separator: " "))"
|
||||
let summary = "\(path.status) \(path.availableInterfaces.map(\.name).joined(separator: " ")) via \(path.gateways.map { "\($0)" }.joined(separator: " "))"
|
||||
Task { @MainActor in
|
||||
self?.pathChanged(summary)
|
||||
}
|
||||
@@ -40,8 +40,12 @@ final class NetworkWatch {
|
||||
}
|
||||
|
||||
private func pathChanged(_ summary: String) {
|
||||
guard seen else {
|
||||
seen = true
|
||||
let first = last == nil
|
||||
guard summary != last else {
|
||||
return
|
||||
}
|
||||
last = summary
|
||||
if first {
|
||||
return
|
||||
}
|
||||
log.notice("network changed (\(summary, privacy: .public)), reconnecting")
|
||||
|
||||
@@ -184,7 +184,6 @@ final class PaneFocus {
|
||||
}
|
||||
|
||||
private func changed(_ name: Notification.Name) {
|
||||
logFocus(name.rawValue)
|
||||
update()
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ private struct SheetPanel: View {
|
||||
let sheet: Sheet
|
||||
let overLeft: Bool
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
|
||||
var body: some View {
|
||||
let task = store.selectedTaskId.flatMap { store.findTask($0) }
|
||||
@@ -129,7 +130,7 @@ private struct SheetPanel: View {
|
||||
case .diagram: "Diagram"
|
||||
}
|
||||
if !overLeft {
|
||||
return "Lead · \(label)"
|
||||
return "\(overlays.leftTab.rawValue.capitalized) · \(label)"
|
||||
}
|
||||
let project = store.focusedProject()?.name ?? ""
|
||||
guard let task else {
|
||||
|
||||
@@ -56,6 +56,8 @@ private let stateTitles: [ByreTask.StatePayload: TaskState] = [
|
||||
.discarded: TaskState(kind: .idle, label: "Discarded", title: "The task was discarded"),
|
||||
]
|
||||
|
||||
private let pausedState = TaskState(kind: .held, label: "Paused", title: "The agent holds after its current step until you resume it")
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class Store {
|
||||
@@ -267,7 +269,10 @@ final class Store {
|
||||
}
|
||||
|
||||
func taskState(_ task: ByreTask) -> TaskState {
|
||||
stateTitles[task.state]!
|
||||
if task.state == .running, (sessions[task.id] ?? []).contains(where: { $0.paused }) {
|
||||
return pausedState
|
||||
}
|
||||
return stateTitles[task.state]!
|
||||
}
|
||||
|
||||
func progress(_ task: ByreTask) -> TaskState? {
|
||||
|
||||
@@ -457,15 +457,11 @@ private struct SwitcherBox: View {
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
#if os(macOS)
|
||||
logFocus("switcher before focus")
|
||||
if model.inList {
|
||||
overlays.window?.makeFirstResponder(nil)
|
||||
} else {
|
||||
focused = true
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
logFocus("switcher after focus focused=\(focused)")
|
||||
}
|
||||
#else
|
||||
focused = !model.inList
|
||||
#endif
|
||||
|
||||
@@ -1114,6 +1114,11 @@ private func resultText(_ content: (any Sendable)?) -> String {
|
||||
if let d = c as? JSONDict, let text = d["text"] {
|
||||
return jsString(text)
|
||||
}
|
||||
if let d = c as? JSONDict, str(d, "type") == "image" {
|
||||
let source = d["source"] as? JSONDict
|
||||
let size = (source?["data"] as? String).map { " " + bytes(Int64($0.utf8.count / 4 * 3)) } ?? ""
|
||||
return "[image\(source.flatMap { str($0, "media_type") }.map { " " + $0 } ?? "")\(size)]"
|
||||
}
|
||||
return compactJSON(c)
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
|
||||
@@ -337,7 +337,10 @@ private struct StepRow: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||
Text(guide).foregroundStyle(Palette.muted)
|
||||
Text(guide)
|
||||
.font(Fonts.font(13, mono: true))
|
||||
.foregroundStyle(Palette.muted)
|
||||
.fixedSize()
|
||||
if node.parent?.kind == .random {
|
||||
WeightField(node: node)
|
||||
}
|
||||
@@ -352,13 +355,14 @@ private struct StepRow: View {
|
||||
.frame(width: 12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
TextField("Stage name", text: $node.name)
|
||||
.textFieldStyle(.plain)
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
if let agents = node.agents {
|
||||
Text("team of \(agents.count)").foregroundStyle(Palette.muted).fixedSize()
|
||||
if touchOnly {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
stageName
|
||||
stageModel
|
||||
}
|
||||
} else {
|
||||
ModelPicker(model: $node.model, effort: $node.effort)
|
||||
stageName
|
||||
stageModel
|
||||
}
|
||||
case .order:
|
||||
Text("in order").foregroundStyle(Palette.muted)
|
||||
@@ -406,6 +410,21 @@ private struct StepRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var stageName: some View {
|
||||
TextField("Stage name", text: $node.name)
|
||||
.textFieldStyle(.plain)
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var stageModel: some View {
|
||||
if let agents = node.agents {
|
||||
Text("team of \(agents.count)").foregroundStyle(Palette.muted).fixedSize()
|
||||
} else {
|
||||
ModelPicker(model: $node.model, effort: $node.effort)
|
||||
}
|
||||
}
|
||||
|
||||
private var actions: some View {
|
||||
Menu {
|
||||
switch node.kind {
|
||||
|
||||
Reference in New Issue
Block a user