Add session details to menui

This commit is contained in:
Peter Steinberger 2025-06-20 16:34:22 +02:00
parent d6d3a8f570
commit c6a9e84bec
3 changed files with 249 additions and 11 deletions

View file

@ -37,6 +37,12 @@ struct MenuBarView: View {
SessionCountView(count: sessionMonitor.sessionCount)
.padding(.horizontal, 12)
.padding(.vertical, 8)
// Session list with clickable items
if !sessionMonitor.sessions.isEmpty {
SessionListView(sessions: sessionMonitor.sessions)
.padding(.horizontal, 4)
}
Divider()
.padding(.vertical, 4)
@ -222,11 +228,12 @@ struct SessionCountView: View {
/// Lists active SSH sessions with truncation for large lists
struct SessionListView: View {
let sessions: [String: SessionMonitor.SessionInfo]
@Environment(\.openWindow) private var openWindow
var body: some View {
VStack(alignment: .leading, spacing: 2) {
ForEach(Array(activeSessions.prefix(5)), id: \.key) { session in
SessionRowView(session: session)
SessionRowView(session: session, openWindow: openWindow)
}
if activeSessions.count > 5 {
@ -251,17 +258,39 @@ struct SessionListView: View {
/// Individual row displaying session information
struct SessionRowView: View {
let session: (key: String, value: SessionMonitor.SessionInfo)
let openWindow: OpenWindowAction
@State private var isHovered = false
var body: some View {
HStack {
Text("\(sessionName)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.middle)
Spacer()
Button(action: {
// Open the session detail window
openWindow(id: "session-detail", value: session.key)
}) {
HStack {
Text("\(sessionName)")
.font(.system(size: 12))
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.middle)
Spacer()
Text("PID: \(session.value.pid)")
.font(.system(size: 11))
.foregroundColor(.secondary)
}
.padding(.vertical, 2)
.padding(.horizontal, 8)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
.background(
RoundedRectangle(cornerRadius: 4)
.fill(isHovered ? Color.accentColor.opacity(0.1) : Color.clear)
)
.onHover { hovering in
isHovered = hovering
}
.padding(.vertical, 2)
}
private var sessionName: String {
@ -270,8 +299,8 @@ struct SessionRowView: View {
let name = (workingDir as NSString).lastPathComponent
// Truncate long session names
if name.count > 35 {
let prefix = String(name.prefix(20))
if name.count > 30 {
let prefix = String(name.prefix(15))
let suffix = String(name.suffix(10))
return "\(prefix)...\(suffix)"
}

View file

@ -0,0 +1,196 @@
import SwiftUI
/// View displaying detailed information about a specific terminal session
struct SessionDetailView: View {
let session: SessionMonitor.SessionInfo
@State private var windowTitle = ""
var body: some View {
VStack(alignment: .leading, spacing: 20) {
// Session Header
VStack(alignment: .leading, spacing: 8) {
Text("Session Details")
.font(.largeTitle)
.fontWeight(.bold)
HStack {
Label("PID: \(session.pid)", systemImage: "number.circle.fill")
.font(.title3)
Spacer()
StatusBadge(isRunning: session.isRunning)
}
}
.padding(.bottom, 10)
// Session Information
VStack(alignment: .leading, spacing: 16) {
DetailRow(label: "Session ID", value: session.id)
DetailRow(label: "Command", value: session.command)
DetailRow(label: "Working Directory", value: session.workingDir)
DetailRow(label: "Status", value: session.status.capitalized)
DetailRow(label: "Started At", value: formatDate(session.startedAt))
DetailRow(label: "Last Modified", value: formatDate(session.lastModified))
if let exitCode = session.exitCode {
DetailRow(label: "Exit Code", value: "\(exitCode)")
}
}
Spacer()
// Action Buttons
HStack {
Button("Open in Terminal") {
openInTerminal()
}
.controlSize(.large)
Spacer()
if session.isRunning {
Button("Terminate Session") {
terminateSession()
}
.controlSize(.large)
.foregroundColor(.red)
}
}
}
.padding(30)
.frame(minWidth: 600, minHeight: 450)
.onAppear {
updateWindowTitle()
}
.background(WindowAccessor(title: $windowTitle))
}
private func updateWindowTitle() {
let dir = URL(fileURLWithPath: session.workingDir).lastPathComponent
windowTitle = "\(dir) — VibeTunnel (PID: \(session.pid))"
}
private func formatDate(_ dateString: String) -> String {
// Parse the date string and format it nicely
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
if let date = formatter.date(from: String(dateString.prefix(19))) {
formatter.dateStyle = .medium
formatter.timeStyle = .medium
return formatter.string(from: date)
}
return dateString
}
private func openInTerminal() {
// TODO: Implement opening session in terminal
print("Open session \(session.id) in terminal")
}
private func terminateSession() {
// TODO: Implement session termination
print("Terminate session \(session.id)")
}
}
// MARK: - Supporting Views
struct DetailRow: View {
let label: String
let value: String
var body: some View {
HStack(alignment: .top) {
Text(label + ":")
.fontWeight(.medium)
.foregroundColor(.secondary)
.frame(width: 140, alignment: .trailing)
Text(value)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
struct StatusBadge: View {
let isRunning: Bool
var body: some View {
HStack(spacing: 6) {
Circle()
.fill(isRunning ? Color.green : Color.red)
.frame(width: 10, height: 10)
Text(isRunning ? "Running" : "Stopped")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(isRunning ? .green : .red)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(isRunning ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
)
}
}
// MARK: - Window Title Accessor
struct WindowAccessor: NSViewRepresentable {
@Binding var title: String
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let window = view.window {
window.title = self.title
// Watch for title changes
Task { @MainActor in
context.coordinator.startObserving(window: window, binding: self.$title)
}
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
DispatchQueue.main.async {
if let window = nsView.window {
window.title = self.title
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject {
private var observation: NSKeyValueObservation?
@MainActor
func startObserving(window: NSWindow, binding: Binding<String>) {
// Update the binding when window title changes
observation = window.observe(\.title, options: [.new]) { window, change in
if let newTitle = change.newValue {
DispatchQueue.main.async {
binding.wrappedValue = newTitle
}
}
}
// Set initial title
window.title = binding.wrappedValue
}
deinit {
observation?.invalidate()
}
}
}

View file

@ -37,6 +37,19 @@ struct VibeTunnelApp: App {
.windowResizability(.contentSize)
.defaultSize(width: 580, height: 480)
.windowStyle(.hiddenTitleBar)
// Session Detail Window
WindowGroup("Session Details", id: "session-detail", for: String.self) { $sessionId in
if let sessionId = sessionId,
let session = SessionMonitor.shared.sessions[sessionId] {
SessionDetailView(session: session)
.withVibeTunnelServices()
} else {
Text("Session not found")
.frame(width: 400, height: 300)
}
}
.windowResizability(.contentSize)
Settings {
SettingsView()