Show the terminal immediately when opening the Terminal tab

This commit is contained in:
Ian Gulliver
2026-10-21 15:05:09 -07:00
parent 47b62763ce
commit 4b4763779d
8 changed files with 420 additions and 414 deletions
+2 -1
View File
@@ -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()
+23
View File
@@ -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
+13 -50
View File
@@ -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 {