Files
clawdbot/apps/macos/Sources/Clawdbot/SessionsSettings.swift
2026-01-04 14:38:51 +00:00

214 lines
7.0 KiB
Swift

import AppKit
import SwiftUI
@MainActor
struct SessionsSettings: View {
private let isPreview: Bool
@State private var rows: [SessionRow]
@State private var errorMessage: String?
@State private var loading = false
@State private var hasLoaded = false
init(rows: [SessionRow]? = nil, isPreview: Bool = ProcessInfo.processInfo.isPreview) {
self._rows = State(initialValue: rows ?? [])
self.isPreview = isPreview
if isPreview {
self._hasLoaded = State(initialValue: true)
}
}
var body: some View {
VStack(alignment: .leading, spacing: 14) {
self.header
self.content
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.task {
guard !self.hasLoaded else { return }
guard !self.isPreview else { return }
self.hasLoaded = true
await self.refresh()
}
}
private var header: some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Sessions")
.font(.headline)
Text("Peek at the stored conversation buckets the CLI reuses for context and rate limits.")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
if self.loading {
ProgressView()
} else {
Button {
Task { await self.refresh() }
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
.buttonStyle(.bordered)
.help("Refresh")
}
}
}
private var content: some View {
Group {
if self.rows.isEmpty, self.errorMessage == nil {
Text("No sessions yet. They appear after the first inbound message or heartbeat.")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.top, 6)
} else {
List(self.rows) { row in
self.sessionRow(row)
}
.listStyle(.inset)
.overlay(alignment: .topLeading) {
if let errorMessage {
Text(errorMessage)
.font(.footnote)
.foregroundStyle(.red)
.padding(.leading, 4)
.padding(.top, 4)
}
}
// The view already applies horizontal padding; keep the list aligned with the text above.
.padding(.horizontal, -12)
}
}
}
@ViewBuilder
private func sessionRow(_ row: SessionRow) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(row.label)
.font(.subheadline.bold())
.lineLimit(1)
.truncationMode(.middle)
Spacer()
Text(row.ageText)
.font(.caption)
.foregroundStyle(.secondary)
}
HStack(spacing: 6) {
if row.kind != .direct {
SessionKindBadge(kind: row.kind)
}
if !row.flagLabels.isEmpty {
ForEach(row.flagLabels, id: \.self) { flag in
Badge(text: flag)
}
}
}
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Text("Context")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Spacer()
Text(row.tokens.contextSummaryShort)
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
ContextUsageBar(
usedTokens: row.tokens.total,
contextTokens: row.tokens.contextTokens,
width: nil)
}
HStack(spacing: 10) {
if let model = row.model, !model.isEmpty {
self.label(icon: "cpu", text: model)
}
self.label(icon: "arrow.down.left", text: "\(row.tokens.input) in")
self.label(icon: "arrow.up.right", text: "\(row.tokens.output) out")
if let sessionId = row.sessionId, !sessionId.isEmpty {
HStack(spacing: 4) {
Image(systemName: "number").foregroundStyle(.secondary).font(.caption)
Text(sessionId)
.font(.footnote.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
.help(sessionId)
}
}
}
.padding(.vertical, 6)
}
private func label(icon: String, text: String) -> some View {
HStack(spacing: 4) {
Image(systemName: icon).foregroundStyle(.secondary).font(.caption)
Text(text)
}
.font(.footnote)
.foregroundStyle(.secondary)
}
private func refresh() async {
guard !self.loading else { return }
guard !self.isPreview else { return }
self.loading = true
self.errorMessage = nil
do {
let snapshot = try await SessionLoader.loadSnapshot()
self.rows = snapshot.rows
} catch {
self.rows = []
self.errorMessage = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
}
self.loading = false
}
}
private struct SessionKindBadge: View {
let kind: SessionKind
var body: some View {
Text(self.kind.label)
.font(.caption2.weight(.bold))
.padding(.horizontal, 7)
.padding(.vertical, 4)
.foregroundStyle(self.kind.tint)
.background(self.kind.tint.opacity(0.15))
.clipShape(Capsule())
}
}
private struct Badge: View {
let text: String
var body: some View {
Text(self.text)
.font(.caption2.weight(.semibold))
.padding(.horizontal, 6)
.padding(.vertical, 3)
.foregroundStyle(.secondary)
.background(Color.secondary.opacity(0.12))
.clipShape(Capsule())
}
}
#if DEBUG
struct SessionsSettings_Previews: PreviewProvider {
static var previews: some View {
SessionsSettings(rows: SessionRow.previewRows, isPreview: true)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif