From 2112fa919afd061629d572fe5f822678debeced3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Dec 2025 01:55:09 +0100 Subject: [PATCH] webchat: fetch remote sessions via CLI and log missing history --- .../macos/Sources/Clawdis/WebChatWindow.swift | 126 ++++++++++++++---- 1 file changed, 101 insertions(+), 25 deletions(-) diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index 3ca118589..81a825318 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -225,17 +225,10 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler, private static func loadInitialMessagesJSON(for sessionKey: String) -> String { // Prefer remote session log when running in remote mode; fall back to local files. var content: String? - if self.connectionModeIsRemote(), - let sessionId = self.remoteSessionId(for: sessionKey) - { - if let data = self.readRemoteFile("$HOME/.clawdis/sessions/\(sessionId).jsonl"), - let text = String(data: data, encoding: .utf8) - { - content = text - } else if let data = self.readRemoteFile("$HOME/.tau/agent/sessions/clawdis/\(sessionId).jsonl"), - let text = String(data: data, encoding: .utf8) - { - content = text + + if self.connectionModeIsRemote() { + if let remote = self.fetchRemoteSessionLog(sessionKey: sessionKey) { + content = remote } } else if let sessionId = self.sessionId(for: sessionKey) { let primary = self.expand("~/.clawdis/sessions/\(sessionId).jsonl") @@ -300,24 +293,68 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler, return (target: target, identity: identity) } - private static func remoteSessionId(for key: String) -> String? { - if let data = self.readRemoteFile("$HOME/.clawdis/sessions/sessions.json"), - let decoded = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let entry = decoded[key] as? [String: Any], - let sessionId = entry["sessionId"] as? String - { - return sessionId + private struct SessionsSummary: Decodable { + struct Entry: Decodable { + let key: String + let sessionId: String? } - if let data = self.readRemoteFile("$HOME/.tau/agent/sessions/clawdis/sessions.json"), - let decoded = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let entry = decoded[key] as? [String: Any], - let sessionId = entry["sessionId"] as? String - { - return sessionId + + let path: String + let sessions: [Entry] + } + + private static func remoteSessionsSummary() -> SessionsSummary? { + guard let jsonData = self.runClawdisCommand(subcommand: "sessions", args: ["--json"]) else { + webChatLogger.error("remote sessions summary command failed") + return nil + } + if let decoded = try? JSONDecoder().decode(SessionsSummary.self, from: jsonData) { + return decoded + } + if let text = String(data: jsonData, encoding: .utf8) { + webChatLogger.error("failed to decode sessions summary json=\(text, privacy: .public)") } return nil } + private static func runClawdisCommand(subcommand: String, args: [String]) -> Data? { + var env = ProcessInfo.processInfo.environment + env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":") + + let command = CommandResolver.clawdisCommand(subcommand: subcommand, extraArgs: args) + let process = Process() + process.executableURL = URL(fileURLWithPath: command.first ?? "/usr/bin/env") + process.arguments = Array(command.dropFirst()) + process.currentDirectoryURL = URL(fileURLWithPath: CommandResolver.projectRootPath()) + process.environment = env + + let out = Pipe() + let err = Pipe() + process.standardOutput = out + process.standardError = err + + do { try process.run() } catch { + webChatLogger.error("clawdis \(subcommand) failed to launch: \(error.localizedDescription, privacy: .public)") + return nil + } + process.waitUntilExit() + let data = out.fileHandleForReading.readDataToEndOfFile() + if !data.isEmpty { return data } + let msg = String(data: err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + webChatLogger.error("clawdis \(subcommand) produced no output: \(msg, privacy: .public)") + return nil + } + + private static func remoteSessionId(for key: String) -> String? { + if let summary = self.remoteSessionsSummary(), + let entry = summary.sessions.first(where: { $0.key == key }), + let sessionId = entry.sessionId + { + return sessionId + } + return self.remoteSessionsSummary()?.sessions.first?.sessionId + } + private static func readRemoteFile(_ path: String) -> Data? { guard let settings = self.remoteSettings(), let parsed = VoiceWakeForwarder.parse(target: settings.target) @@ -344,10 +381,49 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler, do { try process.run() } catch { return nil } process.waitUntilExit() - guard process.terminationStatus == 0 else { return nil } + guard process.terminationStatus == 0 else { + let stderr = String(data: err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + webChatLogger.error("ssh cat failed status=\(process.terminationStatus, privacy: .public) stderr=\(stderr, privacy: .public)") + return nil + } return out.fileHandleForReading.readDataToEndOfFile() } + private static func fetchRemoteSessionLog(sessionKey: String) -> String? { + guard let summary = self.remoteSessionsSummary() else { + webChatLogger.error("remote sessions summary missing") + return nil + } + + let sessionId = summary.sessions.first(where: { $0.key == sessionKey })?.sessionId + ?? summary.sessions.first?.sessionId + guard let sessionId else { + webChatLogger.error("remote session id missing for key=\(sessionKey, privacy: .public)") + return nil + } + + // Prefer the path reported by the CLI; replace trailing sessions.json with the log file. + let basePath = summary.path + let logPath: String = if basePath.hasSuffix("sessions.json") { + String(basePath.dropLast("sessions.json".count)) + "\(sessionId).jsonl" + } else { + basePath + "/\(sessionId).jsonl" + } + + if let data = self.readRemoteFile(logPath), let text = String(data: data, encoding: .utf8) { + return text + } + + // Legacy path fallback if the CLI reports a different store. + let legacy = "$HOME/.tau/agent/sessions/clawdis/\(sessionId).jsonl" + if let data = self.readRemoteFile(legacy), let text = String(data: data, encoding: .utf8) { + return text + } + + webChatLogger.error("remote session log not found at \(logPath, privacy: .public)") + return nil + } + private static func expand(_ path: String) -> String { (path as NSString).expandingTildeInPath }