From b824355cfdd59f381f8bb0857dc95f2a50ed166c Mon Sep 17 00:00:00 2001 From: jay Date: Sat, 16 May 2026 04:17:34 +0200 Subject: [PATCH] feat: T-2.8 StatusBar component + pi state display --- Sources/UI/Status/StatusBar.swift | 99 ++++++++++++++++++++++ Sources/UI/Terminal/MainTerminalView.swift | 44 ++++++---- 2 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 Sources/UI/Status/StatusBar.swift diff --git a/Sources/UI/Status/StatusBar.swift b/Sources/UI/Status/StatusBar.swift new file mode 100644 index 0000000..dce4931 --- /dev/null +++ b/Sources/UI/Status/StatusBar.swift @@ -0,0 +1,99 @@ +// StatusBar.swift +// T-2.8 — Session status bar with pi-state indicator and action buttons. + +import Combine +import SwiftUI + +@MainActor +struct StatusBar: View { + let sessionName: String + let connectionStatus: String // "Connecting…", "Connected", "Disconnected" + @Binding var piState: PiState? + var onSwitcher: (() -> Void)? = nil + var onUnpair: (() -> Void)? = nil + var onSettings: (() -> Void)? = nil + + var body: some View { + VStack(spacing: 0) { + HStack { + // ── Left: session name ────────────────────────────── + Text(sessionName.isEmpty ? " " : sessionName) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + + Spacer() + + // ── Center: pi state / connection status ──────────── + stateIndicator + + Spacer() + + // ── Right: icon buttons ───────────────────────────── + HStack(spacing: 14) { + if onSwitcher != nil { + Button { + onSwitcher?() + } label: { + Image(systemName: "list.bullet") + .font(.caption) + } + } + + if onSettings != nil { + Button { + onSettings?() + } label: { + Image(systemName: "gear") + .font(.caption) + } + } + + if onUnpair != nil { + Button { + onUnpair?() + } label: { + Image(systemName: "x.circle") + .font(.caption) + .foregroundStyle(.red) + } + } + } + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color(uiColor: .systemBackground)) + + Divider() + } + } + + // MARK: - State indicator + + @ViewBuilder + private var stateIndicator: some View { + if let state = piState { + switch state { + case .thinking: + Text("● thinking") + .font(.caption.monospaced()) + .foregroundStyle(.orange) + case .tool: + Text("▶ tool") + .font(.caption.monospaced()) + .foregroundStyle(.blue) + case .awaitingInput: + Text("⏸ awaiting") + .font(.caption.monospaced()) + .foregroundStyle(.yellow) + case .idle: + EmptyView() + } + } else { + Text(connectionStatus) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + } +} diff --git a/Sources/UI/Terminal/MainTerminalView.swift b/Sources/UI/Terminal/MainTerminalView.swift index 235418c..d54a309 100644 --- a/Sources/UI/Terminal/MainTerminalView.swift +++ b/Sources/UI/Terminal/MainTerminalView.swift @@ -12,28 +12,22 @@ struct MainTerminalView: View { @State private var terminalVC = TerminalViewController() @State private var connection: SessionConnection? = nil @State private var statusText = "Connecting…" + @State private var currentPiState: PiState? = nil + @State private var sessionName = "" + @State private var showSwitcher = false @State private var cancellables = Set() @EnvironmentObject var appState: AppState var body: some View { VStack(spacing: 0) { // ── Status bar ────────────────────────────────────────── - HStack { - Text(statusText) - .font(.caption.monospaced()) - .foregroundStyle(.secondary) - Spacer() - Button("Unpair") { - appState.unpair() - } - .font(.caption) - .foregroundStyle(.red) - } - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color(uiColor: .systemBackground)) - - Divider() + StatusBar( + sessionName: sessionName, + connectionStatus: statusText, + piState: $currentPiState, + onSwitcher: { showSwitcher = true }, + onUnpair: { appState.unpair() } + ) // ── Terminal ──────────────────────────────────────────── TerminalViewRepresentable(controller: terminalVC) @@ -129,6 +123,24 @@ struct MainTerminalView: View { } .store(in: &cancellables) + // Wire stateEvents → currentPiState + conn.stateEvents + .compactMap { event -> PiState? in + if case .state(let s, _, _) = event { return s } else { return nil } + } + .receive(on: DispatchQueue.main) + .sink { state in currentPiState = state } + .store(in: &cancellables) + + // Wire stateEvents → sessionName + conn.stateEvents + .compactMap { event -> String? in + if case .sessionMeta(let name, _, _) = event { return name } else { return nil } + } + .receive(on: DispatchQueue.main) + .sink { name in sessionName = name } + .store(in: &cancellables) + connection = conn let lastSeq: UInt64? = conn.scrollback.sizeBytes > 0