away mode visibility
This commit is contained in:
+1
-1
@@ -346,7 +346,7 @@ An agent error (a turn that ends in error without a human interrupt, a process t
|
|||||||
|
|
||||||
### Away
|
### Away
|
||||||
|
|
||||||
A user who is away has byre answer their agents' questions instead of waiting (`internal/agents/away.go`). `users.away`, `away` on `GET /api/me`, is set with `PUT /api/me/away` (`Manager.SetAway`), which records `user.updated`. While it is on, every question filed by `ask_human` in a task the user created is answered at once, as that user, with an `other` response of `awayText`: pick the most likely answer and carry on; fork with the `fork` tool (see "Forking") only when strongly convinced both answers are worth following; don't guess at what only the user can give (a test result, a password, a permission), and carry on with other work or end the turn instead; and no authorization is granted. The tool's result says so, and the answer is delivered like any other, usually mid-turn right after the `ask_human` call, so it gets an `answer` snapshot and the user can **Answer again** with their own answer when back. Turning it on answers the user's tasks' questions already open the same way; turning it off lets new questions wait again. The app's Away button, at the right end of the window's toolbar (a `primaryAction` toolbar item, apart from the server picker), shows on (accent) when the user is away on any connected server and sets all of them to the opposite on click, so a mixed state is always cleared by one click. `sqlite3 byre.db "SELECT login FROM users WHERE away = 1"` lists who is away.
|
A user who is away has byre answer their agents' questions instead of waiting (`internal/agents/away.go`). `users.away`, `away` on `GET /api/me`, is set with `PUT /api/me/away` (`Manager.SetAway`), which records `user.updated`. While it is on, every question filed by `ask_human` in a task the user created is answered at once, as that user, with an `other` response of `awayText`: pick the most likely answer and carry on; fork with the `fork` tool (see "Forking") only when strongly convinced both answers are worth following; don't guess at what only the user can give (a test result, a password, a permission), and carry on with other work or end the turn instead; and no authorization is granted. The tool's result says so, and the answer is delivered like any other, usually mid-turn right after the `ask_human` call, so it gets an `answer` snapshot and the user can **Answer again** with their own answer when back. Turning it on answers the user's tasks' questions already open the same way; turning it off lets new questions wait again. The app's Away button, at the right end of the window's toolbar (a `primaryAction` toolbar item, apart from the server picker), shows on when the user is away on any connected server, as an outlined warn-coloured "AWAY · click when back" pill, and sets all of them to the opposite on click, so a mixed state is always cleared by one click. While away, each time the app becomes active, and when any server finishes connecting while the app is active (so a launch asks too), it asks "You're still away … Are you back?" in a modal; "I'm back" turns Away off on every server, "Stay away" (or Escape) leaves it on. `sqlite3 byre.db "SELECT login FROM users WHERE away = 1"` lists who is away.
|
||||||
|
|
||||||
## Notifications
|
## Notifications
|
||||||
|
|
||||||
|
|||||||
@@ -496,6 +496,31 @@ struct ServerWindow: View {
|
|||||||
adopt(new.filter { !old.contains($0) })
|
adopt(new.filter { !old.contains($0) })
|
||||||
}
|
}
|
||||||
.onAppear(perform: appeared)
|
.onAppear(perform: appeared)
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||||
|
remindAway()
|
||||||
|
}
|
||||||
|
.onChange(of: Set(Servers.shared.origins.filter { Servers.shared.connections[$0]?.store != nil })) { old, new in
|
||||||
|
if !new.isSubset(of: old), NSApp.isActive {
|
||||||
|
remindAway()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func remindAway() {
|
||||||
|
guard awayStores().contains(where: { $0.me?.away == true }), let overlays = connection?.overlays, overlays.modal == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
let content = VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("You're still away")
|
||||||
|
.foregroundStyle(Palette.warn)
|
||||||
|
ModalText(text: "byre is answering your agents' questions for you, telling them to pick the most likely answer. Are you back?")
|
||||||
|
}
|
||||||
|
let choice = await overlays.showModal(content, [ModalButton(label: "Stay away", value: "cancel"), ModalButton(label: "I'm back", value: "back", primary: true)])
|
||||||
|
if choice == "back" {
|
||||||
|
await setAway(false, awayStores())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func appeared() {
|
private func appeared() {
|
||||||
@@ -544,22 +569,39 @@ struct ServerPicker: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func awayStores() -> [Store] {
|
||||||
|
Servers.shared.origins.compactMap { Servers.shared.connections[$0]?.store }
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func setAway(_ away: Bool, _ stores: [Store]) async {
|
||||||
|
for store in stores {
|
||||||
|
do {
|
||||||
|
store.me = try await store.api.client.updateAway(body: .json(.init(away: away))).ok.body.json
|
||||||
|
} catch {
|
||||||
|
log.error("set away: \(errorText(error), privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct AwayToggle: View {
|
private struct AwayToggle: View {
|
||||||
@ViewState private var hover = false
|
@ViewState private var hover = false
|
||||||
@ViewState private var busy = false
|
@ViewState private var busy = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let stores = Servers.shared.origins.compactMap { Servers.shared.connections[$0]?.store }
|
let stores = awayStores()
|
||||||
let away = stores.contains { $0.me?.away == true }
|
let away = stores.contains { $0.me?.away == true }
|
||||||
if !stores.isEmpty {
|
if !stores.isEmpty {
|
||||||
Button {
|
Button {
|
||||||
set(!away, stores)
|
set(!away, stores)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Away")
|
Text(away ? "AWAY · click when back" : "Away")
|
||||||
.foregroundStyle(away ? Palette.accent : hover ? Palette.text : Palette.muted)
|
.foregroundStyle(away ? Palette.warn : hover ? Palette.text : Palette.muted)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, away ? 12 : 8)
|
||||||
.padding(.vertical, 3)
|
.padding(.vertical, 3)
|
||||||
.background(away ? Palette.accentTint : .clear, in: RoundedRectangle(cornerRadius: 6))
|
.background(away ? Palette.warnTint : .clear, in: RoundedRectangle(cornerRadius: 6))
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 6).stroke(away ? Palette.warn : .clear))
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -575,13 +617,7 @@ private struct AwayToggle: View {
|
|||||||
private func set(_ away: Bool, _ stores: [Store]) {
|
private func set(_ away: Bool, _ stores: [Store]) {
|
||||||
busy = true
|
busy = true
|
||||||
Task {
|
Task {
|
||||||
for store in stores {
|
await setAway(away, stores)
|
||||||
do {
|
|
||||||
store.me = try await store.api.client.updateAway(body: .json(.init(away: away))).ok.body.json
|
|
||||||
} catch {
|
|
||||||
log.error("set away: \(errorText(error), privacy: .public)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
busy = false
|
busy = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user