From f34b238713452138d62ebddf3685ab1395b1df9f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 21:32:21 +0100 Subject: [PATCH] Debug menu: session controls and thinking/verbose --- apps/macos/Sources/Clawdis/DebugActions.swift | 67 +++++++++++++++++++ .../Sources/Clawdis/MenuContentView.swift | 52 ++++++++++++++ apps/macos/Sources/Clawdis/SessionData.swift | 2 +- 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/apps/macos/Sources/Clawdis/DebugActions.swift b/apps/macos/Sources/Clawdis/DebugActions.swift index 0f3ace993..1b92360e3 100644 --- a/apps/macos/Sources/Clawdis/DebugActions.swift +++ b/apps/macos/Sources/Clawdis/DebugActions.swift @@ -4,6 +4,7 @@ import SwiftUI enum DebugActions { private static let verboseDefaultsKey = "clawdis.debug.verboseMain" + private static let sessionMenuLimit = 12 @MainActor static func openAgentEventsWindow() { @@ -183,6 +184,72 @@ enum DebugActions { } return path } + + // MARK: - Sessions (thinking / verbose) + + static func recentSessions(limit: Int = sessionMenuLimit) async -> [SessionRow] { + let hints = SessionLoader.configHints() + let store = SessionLoader.resolveStorePath(override: hints.storePath) + let defaults = SessionDefaults( + model: hints.model ?? SessionLoader.fallbackModel, + contextTokens: hints.contextTokens ?? SessionLoader.fallbackContextTokens) + guard let rows = try? await SessionLoader.loadRows(at: store, defaults: defaults) else { return [] } + return Array(rows.prefix(limit)) + } + + static func updateSession( + key: String, + thinking: String?, + verbose: String?) async throws + { + let hints = SessionLoader.configHints() + let store = SessionLoader.resolveStorePath(override: hints.storePath) + let url = URL(fileURLWithPath: store) + guard FileManager.default.fileExists(atPath: store) else { + throw DebugActionError.message("Session store missing at \(store)") + } + + let data = try Data(contentsOf: url) + var decoded = try JSONDecoder().decode([String: SessionEntryRecord].self, from: data) + var entry = decoded[key] ?? SessionEntryRecord( + sessionId: nil, + updatedAt: Date().timeIntervalSince1970 * 1000, + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: nil, + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + model: nil, + contextTokens: nil) + + entry = SessionEntryRecord( + sessionId: entry.sessionId, + updatedAt: Date().timeIntervalSince1970 * 1000, + systemSent: entry.systemSent, + abortedLastRun: entry.abortedLastRun, + thinkingLevel: thinking, + verboseLevel: verbose, + inputTokens: entry.inputTokens, + outputTokens: entry.outputTokens, + totalTokens: entry.totalTokens, + model: entry.model, + contextTokens: entry.contextTokens) + + decoded[key] = entry + let encoded = try JSONEncoder().encode(decoded) + try encoded.write(to: url, options: [.atomic]) + } + + @MainActor + static func openSessionStoreInCode() { + let path = SessionLoader.defaultStorePath + let proc = Process() + proc.launchPath = "/usr/bin/env" + proc.arguments = ["code", path] + try? proc.run() + } } enum DebugActionError: LocalizedError { diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index 0cdfb8fc5..853d2d04c 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -15,6 +15,7 @@ struct MenuContent: View { @Environment(\.openSettings) private var openSettings @State private var availableMics: [AudioInputDevice] = [] @State private var loadingMics = false + @State private var sessionMenu: [SessionRow] = [] var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -43,6 +44,49 @@ struct MenuContent: View { } if self.state.debugPaneEnabled { Menu("Debug") { + Menu("Sessions") { + ForEach(self.sessionMenu) { row in + Menu(row.key) { + Menu("Thinking") { + ForEach(["low", "medium", "high", "default"], id: \.self) { level in + let normalized = level == "default" ? nil : level + Button { + Task { + try? await DebugActions.updateSession( + key: row.key, + thinking: normalized, + verbose: row.verboseLevel) + await self.reloadSessionMenu() + } + } label: { + Label(level.capitalized, systemImage: row.thinkingLevel == normalized ? "checkmark" : "") + } + } + } + Menu("Verbose") { + ForEach(["on", "off", "default"], id: \.self) { level in + let normalized = level == "default" ? nil : level + Button { + Task { + try? await DebugActions.updateSession( + key: row.key, + thinking: row.thinkingLevel, + verbose: normalized) + await self.reloadSessionMenu() + } + } label: { + Label(level.capitalized, systemImage: row.verboseLevel == normalized ? "checkmark" : "") + } + } + } + Button { + DebugActions.openSessionStoreInCode() + } label: { + Label("Open Session Log", systemImage: "doc.text") + } + } + } + } Button { DebugActions.openConfigFolder() } label: { @@ -113,6 +157,9 @@ struct MenuContent: View { await self.loadMicrophones(force: true) } } + .task { + await self.reloadSessionMenu() + } .task { VoicePushToTalkHotkey.shared.setEnabled(voiceWakeSupported && self.state.voicePushToTalkEnabled) } @@ -306,6 +353,11 @@ struct MenuContent: View { return "System default" } + @MainActor + private func reloadSessionMenu() async { + self.sessionMenu = await DebugActions.recentSessions() + } + @MainActor private func loadMicrophones(force: Bool = false) async { guard self.showVoiceWakeMicPicker else { diff --git a/apps/macos/Sources/Clawdis/SessionData.swift b/apps/macos/Sources/Clawdis/SessionData.swift index 900765e33..4075d591a 100644 --- a/apps/macos/Sources/Clawdis/SessionData.swift +++ b/apps/macos/Sources/Clawdis/SessionData.swift @@ -1,7 +1,7 @@ import Foundation import SwiftUI -struct SessionEntryRecord: Decodable { +struct SessionEntryRecord: Codable { let sessionId: String? let updatedAt: Double? let systemSent: Bool?