Show the terminal immediately when opening the Terminal tab
This commit is contained in:
@@ -236,7 +236,8 @@ final class Connection {
|
||||
async let inbox: Void = store.loadInbox()
|
||||
async let lead: Void = store.loadLeadSessions()
|
||||
async let reads: Void = store.loadSummaryReads()
|
||||
_ = try await (me, projects, inbox, lead, reads)
|
||||
async let terminals: Void = store.loadTerminals()
|
||||
_ = try await (me, projects, inbox, lead, reads, terminals)
|
||||
try await store.loadFocus()
|
||||
guard !stopped else {
|
||||
await store.stop()
|
||||
|
||||
@@ -60,6 +60,7 @@ final class Store {
|
||||
var tasks: [String: [ByreTask]] = [:]
|
||||
var sessions: [String: [AgentSession]] = [:]
|
||||
var inbox: [InboxItem] = []
|
||||
private(set) var terminals: [String: [TaskTerminal]] = [:]
|
||||
private(set) var selectedTaskId: String?
|
||||
var pinned: Int?
|
||||
var connected = false
|
||||
@@ -85,6 +86,7 @@ final class Store {
|
||||
private var taskFeeds: [String: Feed] = [:]
|
||||
@ObservationIgnored private var historyLoads: Set<ObjectIdentifier> = []
|
||||
@ObservationIgnored private var requestSeqs: [String: Int] = [:]
|
||||
@ObservationIgnored private var terminalChanges = 0
|
||||
@ObservationIgnored private var globalStream: EventStream?
|
||||
@ObservationIgnored private var leadStream: EventStream?
|
||||
@ObservationIgnored private var stopped = false
|
||||
@@ -367,6 +369,20 @@ final class Store {
|
||||
drafts?.prune(self)
|
||||
}
|
||||
|
||||
func loadTerminals() async throws {
|
||||
let latest = claimRequest("terminals")
|
||||
let changes = terminalChanges
|
||||
let list = try await api.client.listTerminals().ok.body.json
|
||||
if !latest() {
|
||||
return
|
||||
}
|
||||
if changes != terminalChanges {
|
||||
try await loadTerminals()
|
||||
return
|
||||
}
|
||||
terminals = Dictionary(grouping: list.terminals, by: \.taskId)
|
||||
}
|
||||
|
||||
func loadLeadSessions() async throws {
|
||||
leadSessions = try await api.client.listLeadSessions().ok.body.json
|
||||
}
|
||||
@@ -607,6 +623,9 @@ final class Store {
|
||||
}, onState: { [weak self] up in
|
||||
self?.connected = up
|
||||
if up {
|
||||
self?.spawn("load terminals") {
|
||||
try await self?.loadTerminals()
|
||||
}
|
||||
self?.paneFocus.reconnected()
|
||||
reconnected()
|
||||
}
|
||||
@@ -632,6 +651,10 @@ final class Store {
|
||||
taskFeeds[taskId]?.push(ev)
|
||||
confirmSend(taskId, ev)
|
||||
}
|
||||
if ev.kind == "terminals.changed", let taskId = ev.taskId {
|
||||
terminalChanges += 1
|
||||
terminals[taskId] = try! decodePayload(ev.payload, as: TerminalList.self).terminals
|
||||
}
|
||||
if ev.seq == 0 {
|
||||
events.send(ev)
|
||||
return
|
||||
|
||||
@@ -7,6 +7,7 @@ let exitedCode = 4000
|
||||
private let retryAfter = Duration.seconds(2)
|
||||
|
||||
typealias TaskTerminal = Components.Schemas.TaskTerminal
|
||||
typealias TerminalList = Components.Schemas.TerminalList
|
||||
typealias SavedCommand = Components.Schemas.SavedCommand
|
||||
|
||||
@MainActor
|
||||
@@ -17,24 +18,23 @@ func terminalFocused() -> Bool {
|
||||
struct TerminalTab: View {
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var list: [TaskTerminal]?
|
||||
@ViewState private var commands: [SavedCommand] = []
|
||||
@ViewState private var selected: String?
|
||||
@ViewState private var status = ""
|
||||
@ViewState private var failure: String?
|
||||
@ViewState private var started = false
|
||||
@ViewState private var loads = 0
|
||||
@ViewState private var starting = false
|
||||
@ViewState private var off: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
let taskId = store.selectedTaskId
|
||||
let task = taskId.flatMap { store.findTask($0) }
|
||||
let current = list?.first { $0.id == selected }?.id ?? list?.first?.id
|
||||
let list = taskId.flatMap { store.terminals[$0] } ?? []
|
||||
let current = list.first { $0.id == selected }?.id ?? list.first?.id
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
ForEach(list ?? [], id: \.id) { term in
|
||||
ForEach(list, id: \.id) { term in
|
||||
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
||||
RangeTabButton(label: term.name, active: term.id == current, help: "Show this terminal") {
|
||||
selected = term.id
|
||||
@@ -77,13 +77,11 @@ struct TerminalTab: View {
|
||||
store?.findTask(taskId)?.status == .active
|
||||
}, show: { text in
|
||||
status = text
|
||||
}, lost: {
|
||||
load()
|
||||
})
|
||||
.id("\(taskId) \(current)")
|
||||
} else if let failure {
|
||||
EmptyText(text: failure, error: true)
|
||||
} else if list != nil {
|
||||
} else if !starting {
|
||||
EmptyText(text: "No terminals are open in this task.")
|
||||
}
|
||||
} else if taskId == nil {
|
||||
@@ -98,23 +96,16 @@ struct TerminalTab: View {
|
||||
.background(Palette.bg)
|
||||
}
|
||||
.onChange(of: "\(taskId ?? "") \(task?.status.rawValue ?? "")", initial: true) {
|
||||
list = nil
|
||||
selected = nil
|
||||
status = ""
|
||||
failure = nil
|
||||
started = false
|
||||
load()
|
||||
}
|
||||
.onChange(of: store.connected) {
|
||||
if store.connected {
|
||||
load()
|
||||
starting = false
|
||||
if task?.status == .active && list.isEmpty {
|
||||
start(nil)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
off = store.events.add { ev in
|
||||
if ev.kind == "terminals.changed" && ev.taskId == store.selectedTaskId {
|
||||
load()
|
||||
}
|
||||
if ev.kind == "command.saved" || ev.kind == "command.deleted" {
|
||||
loadCommands()
|
||||
}
|
||||
@@ -124,30 +115,6 @@ struct TerminalTab: View {
|
||||
.onDisappear {
|
||||
off?()
|
||||
off = nil
|
||||
loads += 1
|
||||
}
|
||||
}
|
||||
|
||||
private func load() {
|
||||
guard let taskId = store.selectedTaskId, store.findTask(taskId)?.status == .active else {
|
||||
return
|
||||
}
|
||||
loads += 1
|
||||
let n = loads
|
||||
Task {
|
||||
do {
|
||||
let loaded = try await store.api.client.listTaskTerminals(path: .init(taskId: taskId)).ok.body.json
|
||||
if n != loads {
|
||||
return
|
||||
}
|
||||
list = loaded
|
||||
failure = nil
|
||||
if loaded.isEmpty && !started {
|
||||
start(nil)
|
||||
}
|
||||
} catch {
|
||||
failure = errorText(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,17 +132,17 @@ struct TerminalTab: View {
|
||||
guard let taskId = store.selectedTaskId else {
|
||||
return
|
||||
}
|
||||
started = true
|
||||
starting = true
|
||||
Task {
|
||||
do {
|
||||
let term = try await store.api.client.startTaskTerminal(path: .init(taskId: taskId), body: .json(.init(commandId: commandId))).created.body.json
|
||||
if store.selectedTaskId == taskId {
|
||||
selected = term.id
|
||||
load()
|
||||
}
|
||||
} catch {
|
||||
failure = errorText(error)
|
||||
}
|
||||
starting = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,11 +166,10 @@ struct TerminalHost: NSViewRepresentable {
|
||||
let terminalId: String
|
||||
let active: @MainActor () -> Bool
|
||||
let show: @MainActor (String) -> Void
|
||||
let lost: @MainActor () -> Void
|
||||
@Environment(\.colorScheme) private var scheme
|
||||
|
||||
func makeCoordinator() -> TerminalSession {
|
||||
TerminalSession(api: api, taskId: taskId, terminalId: terminalId, active: active, show: show, lost: lost)
|
||||
TerminalSession(api: api, taskId: taskId, terminalId: terminalId, active: active, show: show)
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> TerminalView {
|
||||
@@ -261,20 +227,18 @@ final class TerminalSession: NSObject, TerminalViewDelegate, URLSessionWebSocket
|
||||
let terminalId: String
|
||||
let active: @MainActor () -> Bool
|
||||
let show: @MainActor (String) -> Void
|
||||
let lost: @MainActor () -> Void
|
||||
weak var view: TerminalView?
|
||||
var scheme: ColorScheme?
|
||||
private var ws: URLSessionWebSocketTask?
|
||||
private var retry: Task<Void, Never>?
|
||||
private var done = false
|
||||
|
||||
init(api: API, taskId: String, terminalId: String, active: @escaping @MainActor () -> Bool, show: @escaping @MainActor (String) -> Void, lost: @escaping @MainActor () -> Void) {
|
||||
init(api: API, taskId: String, terminalId: String, active: @escaping @MainActor () -> Bool, show: @escaping @MainActor (String) -> Void) {
|
||||
self.api = api
|
||||
self.taskId = taskId
|
||||
self.terminalId = terminalId
|
||||
self.active = active
|
||||
self.show = show
|
||||
self.lost = lost
|
||||
}
|
||||
|
||||
func connect() {
|
||||
@@ -345,7 +309,6 @@ final class TerminalSession: NSObject, TerminalViewDelegate, URLSessionWebSocket
|
||||
return
|
||||
}
|
||||
show("disconnected, reconnecting…")
|
||||
lost()
|
||||
retry = Task {
|
||||
try? await Task.sleep(for: retryAfter)
|
||||
if !Task.isCancelled {
|
||||
|
||||
Reference in New Issue
Block a user