feat: add recent session switchers

This commit is contained in:
Peter Steinberger
2026-01-01 23:45:56 +01:00
parent c7c13f2d5e
commit 7c0379ce05
14 changed files with 427 additions and 35 deletions

View File

@@ -11,6 +11,7 @@ import UniformTypeIdentifiers
struct ClawdisChatComposer: View {
@Bindable var viewModel: ClawdisChatViewModel
let style: ClawdisChatView.Style
let showsSessionSwitcher: Bool
#if !os(macOS)
@State private var pickerItems: [PhotosPickerItem] = []
@@ -23,6 +24,9 @@ struct ClawdisChatComposer: View {
VStack(alignment: .leading, spacing: 4) {
if self.showsToolbar {
HStack(spacing: 6) {
if self.showsSessionSwitcher {
self.sessionPicker
}
self.thinkingPicker
Spacer()
self.refreshButton
@@ -91,6 +95,26 @@ struct ClawdisChatComposer: View {
.frame(maxWidth: 140, alignment: .leading)
}
private var sessionPicker: some View {
Picker(
"Session",
selection: Binding(
get: { self.viewModel.sessionKey },
set: { next in self.viewModel.switchSession(to: next) }))
{
ForEach(self.viewModel.sessionChoices, id: \.key) { session in
Text(session.key)
.font(.system(.caption, design: .monospaced))
.tag(session.key)
}
}
.labelsHidden()
.pickerStyle(.menu)
.controlSize(.small)
.frame(maxWidth: 160, alignment: .leading)
.help("Session")
}
@ViewBuilder
private var attachmentPicker: some View {
#if os(macOS)

View File

@@ -58,7 +58,10 @@ public struct ClawdisChatView: View {
VStack(spacing: Layout.stackSpacing) {
self.messageList
.padding(.horizontal, Layout.outerPaddingHorizontal)
ClawdisChatComposer(viewModel: self.viewModel, style: self.style)
ClawdisChatComposer(
viewModel: self.viewModel,
style: self.style,
showsSessionSwitcher: self.showsSessionSwitcher)
.padding(.horizontal, Layout.composerPaddingHorizontal)
}
.padding(.vertical, Layout.outerPaddingVertical)

View File

@@ -99,6 +99,42 @@ public final class ClawdisChatViewModel {
Task { await self.performSwitchSession(to: sessionKey) }
}
public var sessionChoices: [ClawdisChatSessionEntry] {
let now = Date().timeIntervalSince1970 * 1000
let cutoff = now - (24 * 60 * 60 * 1000)
let sorted = self.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
var seen = Set<String>()
var recent: [ClawdisChatSessionEntry] = []
for entry in sorted {
guard !seen.contains(entry.key) else { continue }
seen.insert(entry.key)
guard (entry.updatedAt ?? 0) >= cutoff else { continue }
recent.append(entry)
}
let mainKey = "main"
var result: [ClawdisChatSessionEntry] = []
var included = Set<String>()
if let main = sorted.first(where: { $0.key == mainKey }) {
result.append(main)
included.insert(mainKey)
} else if self.sessionKey == mainKey {
result.append(self.placeholderSession(key: mainKey))
included.insert(mainKey)
}
for entry in recent where !included.contains(entry.key) {
result.append(entry)
included.insert(entry.key)
}
if !included.contains(self.sessionKey) {
result.append(self.placeholderSession(key: self.sessionKey))
}
return result
}
public func addAttachments(urls: [URL]) {
Task { await self.loadAttachments(urls: urls) }
}
@@ -301,6 +337,23 @@ public final class ClawdisChatViewModel {
await self.bootstrap()
}
private func placeholderSession(key: String) -> ClawdisChatSessionEntry {
ClawdisChatSessionEntry(
key: key,
kind: nil,
updatedAt: nil,
sessionId: nil,
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: nil,
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
model: nil,
contextTokens: nil)
}
private func handleTransportEvent(_ evt: ClawdisChatTransportEvent) {
switch evt {
case let .health(ok):