Tabs: '+' opens brov-chat or term.macOS (max 6, split around the camera); real zsh terminals via SwiftTerm; per-tab chat history and claude session; hover 3 s to close with confirmation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-07 15:27:02 +03:00
parent 0d40b78b26
commit 56367ef9cd
11 changed files with 452 additions and 35 deletions
+64 -2
View File
@@ -9,7 +9,26 @@ final class AppState: ObservableObject {
// Island state
@Published var mode: IslandMode = .hidden
@Published var view: IslandView = .overview
@Published var view: IslandView = .overview {
didSet {
// Any way into the chat (drop → "Задать вопрос", shortcuts…) lands in a brov-chat tab.
if view == .prompt, tabs.first(where: { $0.id == activeTabId })?.kind != .chat {
if let chat = tabs.first(where: { $0.kind == .chat }) {
if let cur = activeTabId, tabs.first(where: { $0.id == cur })?.kind == .chat {
ChatTabs.shared.saveHistory(chatHistory, for: cur)
}
activeTabId = chat.id
chatHistory = ChatTabs.shared.history(for: chat.id)
} else if tabs.count < Self.maxTabs {
let n = (1...Self.maxTabs).first { k in !tabs.contains { $0.kind == .chat && $0.number == k } } ?? 1
let t = BroTab(kind: .chat, number: n)
tabs.append(t)
activeTabId = t.id
chatHistory = []
}
}
}
}
// Tasks
@Published var tasks: [AgentTask] = []
@@ -420,7 +439,7 @@ final class AppState: ObservableObject {
}
func chatDetentHeight(_ d: Int) -> CGFloat {
let auto = min(300, 240 + CGFloat(chatHistory.count) * 40)
let auto = view == .terminal ? 380 : min(300, 240 + CGFloat(chatHistory.count) * 40)
switch d {
case 1: return max(320, screenHeight / 3)
case 2: return max(340, screenHeight / 2)
@@ -432,6 +451,48 @@ final class AppState: ObservableObject {
/// Current chat island height (drag, else detent).
var chatIslandHeight: CGFloat { chatDragHeight ?? chatDetentHeight(chatDetent) }
// MARK: Tabs (BroV): brov-chat dialogs and term.macOS terminals
static let maxTabs = 6
@Published var tabs: [BroTab] = [BroTab(kind: .chat, number: 1)]
@Published var activeTabId: UUID? = nil
func openTab(_ kind: TabKind) {
guard tabs.count < Self.maxTabs else { return }
let used = Set(tabs.filter { $0.kind == kind }.map(\.number))
let n = (1...Self.maxTabs).first { !used.contains($0) } ?? tabs.count + 1
let tab = BroTab(kind: kind, number: n)
tabs.append(tab)
SoundEngine.shared.play("pop")
activateTab(tab.id)
}
func activateTab(_ id: UUID) {
guard let tab = tabs.first(where: { $0.id == id }) else { return }
if let cur = activeTabId, cur != id, tabs.first(where: { $0.id == cur })?.kind == .chat {
ChatTabs.shared.saveHistory(chatHistory, for: cur)
}
if activeTabId != id {
activeTabId = id
if tab.kind == .chat { chatHistory = ChatTabs.shared.history(for: id) }
}
view = tab.kind == .chat ? .prompt : .terminal
}
func closeTab(_ id: UUID) {
guard let i = tabs.firstIndex(where: { $0.id == id }) else { return }
let tab = tabs.remove(at: i)
if tab.kind == .chat { ChatTabs.shared.close(id) } else { TerminalStore.shared.close(id) }
SoundEngine.shared.play("close")
guard activeTabId == id else { return }
activeTabId = nil
if tab.kind == .chat { chatHistory = [] }
if let next = tabs.indices.contains(i) ? tabs[i] : tabs.last {
activateTab(next.id)
} else {
view = .overview
}
}
// MARK: Agent health (BroV)
@Published var crashedSessions: [CrashedSession] = []
@Published var claudeNeedsLogin = false
@@ -466,6 +527,7 @@ final class AppState: ObservableObject {
// MARK: - Init (loads persisted settings)
private init() {
activeTabId = tabs.first?.id
let ud = UserDefaults.standard
if let v = ud.object(forKey: "soundEnabled") as? Bool { soundEnabled = v }