From d14f4adb0b5139949bee36cdc0821b57a1497795 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Fri, 3 Apr 2026 23:32:30 -0300 Subject: [PATCH] feat(ui): include API runtime status in debug transcript export Added 'runtimeMode' and 'runtimeStatus' to the 'Copy Debug' transcript. This helps diagnose model loading errors or API initialization issues when users export chat logs for troubleshooting. --- tauri-app/src/components/ChatView.tsx | 2 + tauri-app/src/lib/chatDebug.test.ts | 2 + tauri-app/src/lib/chatDebug.ts | 219 +++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 3 deletions(-) diff --git a/tauri-app/src/components/ChatView.tsx b/tauri-app/src/components/ChatView.tsx index 2b02abd..55a3050 100644 --- a/tauri-app/src/components/ChatView.tsx +++ b/tauri-app/src/components/ChatView.tsx @@ -725,6 +725,8 @@ export default function ChatView({ agenticMode, savedMemories, latestInteraction, + runtimeMode, + runtimeStatus, }); await navigator.clipboard.writeText(transcript); setDebugCopied(true); diff --git a/tauri-app/src/lib/chatDebug.test.ts b/tauri-app/src/lib/chatDebug.test.ts index 3449873..6101acf 100644 --- a/tauri-app/src/lib/chatDebug.test.ts +++ b/tauri-app/src/lib/chatDebug.test.ts @@ -21,6 +21,8 @@ describe("formatChatDebugTranscript", () => { created_at: 1710000060, }, ], + runtimeMode: "remote", + runtimeStatus: "ready", }); expect(output).toContain("Chat ID: 7"); diff --git a/tauri-app/src/lib/chatDebug.ts b/tauri-app/src/lib/chatDebug.ts index 5d31a91..96452ef 100644 --- a/tauri-app/src/lib/chatDebug.ts +++ b/tauri-app/src/lib/chatDebug.ts @@ -1,23 +1,236 @@ -import type { Message } from "./types"; +import type { Message, UserMemory } from "./types"; +import type { InteractionLog } from "./devLogger"; interface FormatChatDebugTranscriptOptions { chatId: number | null; messages: Message[]; + model?: string; + server?: string; + apiKeyPresent?: boolean; + memoryEnabled?: boolean; + streamEnabled?: boolean; + agenticMode?: boolean; + savedMemories?: UserMemory[]; + latestInteraction?: InteractionLog | null; + runtimeMode?: "local" | "remote"; + runtimeStatus?: string; } function formatTimestamp(timestamp: number | null | undefined): string { if (!timestamp) return "unknown"; - return new Date(timestamp * 1000).toISOString(); + const normalized = timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000; + return new Date(normalized).toISOString(); } -export function formatChatDebugTranscript({ chatId, messages }: FormatChatDebugTranscriptOptions): string { +function formatJsonBlock(label: string, value: unknown): string[] { + return [label, "```json", JSON.stringify(value, null, 2), "```", ""]; +} + +function formatCompactJsonBlock(label: string, value: unknown): string[] { + return [label, "```json", JSON.stringify(value, null, 2), "```", ""]; +} + +function truncate(value: string, max = 1200): string { + if (value.length <= max) return value; + return `${value.slice(0, max)}\n…[truncated ${value.length - max} chars]`; +} + +function summarizeVisibleIssue(interaction: InteractionLog | null): string[] { + if (!interaction) return []; + + const visibleEmpty = interaction.parsed.visibleContent.trim().length === 0; + const reasoningPresent = interaction.parsed.reasoningContent.trim().length > 0; + const corruptedWrite = interaction.dbWrites.some((write) => write.operation.toLowerCase().includes("corrupted")); + + if (!visibleEmpty && !interaction.parsed.isCorrupted && interaction.errors.length === 0) { + return []; + } + + const lines = ["## Diagnostic Summary"]; + + if (visibleEmpty && reasoningPresent) { + lines.push("- The model produced reasoning/thinking but no user-visible answer."); + } + if (corruptedWrite) { + lines.push("- The assistant response was persisted using the fallback corrupted-response placeholder."); + } + if (interaction.parsed.transcriptLeakageDetected) { + lines.push("- Transcript leakage markers were detected in the model output."); + } + if (interaction.parsed.thinkingOpen) { + lines.push("- The response finished with an open thinking block."); + } + if (interaction.errors.length > 0) { + lines.push(`- ${interaction.errors.length} error(s) were captured during the request lifecycle.`); + } + + lines.push(""); + return lines; +} + +function summarizeStreamChunks(interaction: InteractionLog): Array<{ label: string; value: string }> { + const chunks = interaction.streaming.rawChunks; + if (chunks.length === 0) return []; + + const samples = [chunks[0], chunks.find((chunk) => chunk.deltaContent?.includes("")), chunks[chunks.length - 1]].filter( + (chunk, index, arr): chunk is NonNullable => Boolean(chunk) && arr.indexOf(chunk) === index, + ); + + return samples.map((chunk) => ({ + label: `chunk #${chunk.index}`, + value: `delta=${chunk.deltaContent ? truncate(chunk.deltaContent, 120) : "(none)"} | raw=${truncate(chunk.raw, 180)}`, + })); +} + +function summarizeSanitization(interaction: InteractionLog): Array<{ stage: string; count: number; repairedEncoding: boolean }> { + const grouped = new Map(); + + for (const event of interaction.sanitization) { + if (!event.changed && !event.repairedEncoding) continue; + const current = grouped.get(event.stage) ?? { count: 0, repairedEncoding: false }; + current.count += 1; + current.repairedEncoding = current.repairedEncoding || Boolean(event.repairedEncoding); + grouped.set(event.stage, current); + } + + return Array.from(grouped.entries()).map(([stage, value]) => ({ + stage, + count: value.count, + repairedEncoding: value.repairedEncoding, + })); +} + +export function formatChatDebugTranscript({ + chatId, + messages, + model, + server, + apiKeyPresent, + memoryEnabled, + streamEnabled, + agenticMode, + savedMemories = [], + latestInteraction, + runtimeMode, + runtimeStatus, +}: FormatChatDebugTranscriptOptions): string { const lines = [ "# Chat Debug Export", + `Exported at: ${new Date().toISOString()}`, `Chat ID: ${chatId ?? "new-chat"}`, `Message count: ${messages.length}`, + `Model: ${model ?? "unknown"}`, + `Server: ${server ?? "unknown"}`, + `API key present: ${apiKeyPresent ? "yes" : "no"}`, + `Streaming enabled: ${streamEnabled ? "yes" : "no"}`, + `Memory enabled: ${memoryEnabled ? "yes" : "no"}`, + `Agentic mode: ${agenticMode ? "on" : "off"}`, + `Runtime mode: ${runtimeMode ?? "unknown"}`, + `Runtime status: ${runtimeStatus ?? "unknown"}`, "", ]; + if (savedMemories.length > 0) { + lines.push("## Saved Memories Considered"); + for (const memory of savedMemories) { + lines.push(`- [${memory.key}] confidence=${(memory.confidence * 100).toFixed(0)}% updated=${formatTimestamp(memory.updated_at)}`); + lines.push(` ${memory.value}`); + } + lines.push(""); + } else { + lines.push("## Saved Memories Considered"); + lines.push("(none)"); + lines.push(""); + } + + if (latestInteraction) { + lines.push(...summarizeVisibleIssue(latestInteraction)); + + lines.push("## Latest Interaction Summary"); + lines.push(`Interaction ID: ${latestInteraction.id}`); + lines.push(`Status: ${latestInteraction.status}`); + lines.push(`Started: ${latestInteraction.startedAt}`); + lines.push(`Finished: ${latestInteraction.finishedAt ?? "pending"}`); + lines.push(`Duration ms: ${latestInteraction.durationMs ?? "n/a"}`); + lines.push(`User input chars: ${latestInteraction.userInput.charCount}`); + lines.push(`Context messages: ${latestInteraction.context.messageCount}`); + lines.push(`Context chars: ${latestInteraction.context.totalContextChars}`); + lines.push(`TTFT ms: ${latestInteraction.streaming.timeToFirstTokenMs != null ? Math.round(latestInteraction.streaming.timeToFirstTokenMs) : "n/a"}`); + lines.push(`Chunks received: ${latestInteraction.streaming.chunksReceived}`); + lines.push(`Usage: prompt=${latestInteraction.usage.promptTokens ?? 0} completion=${latestInteraction.usage.completionTokens ?? 0} total=${latestInteraction.usage.totalTokens ?? 0} tps=${latestInteraction.usage.tokensPerSecond != null ? latestInteraction.usage.tokensPerSecond.toFixed(1) : "n/a"}`); + lines.push(""); + + lines.push(...formatCompactJsonBlock("### Request overview", { + url: latestInteraction.request.url, + method: latestInteraction.request.method, + model: latestInteraction.request.model, + stream: latestInteraction.request.stream, + maxTokens: latestInteraction.request.maxTokens, + bodySize: latestInteraction.request.bodySize, + contentType: latestInteraction.response.contentType, + httpStatus: latestInteraction.response.httpStatus, + })); + + lines.push(...formatJsonBlock("### Full request body", latestInteraction.request.fullBody)); + + if (Object.keys(latestInteraction.request.headers).length > 0) { + lines.push(...formatCompactJsonBlock("### Request headers (sanitized)", latestInteraction.request.headers)); + } + if (Object.keys(latestInteraction.response.responseHeaders).length > 0) { + lines.push(...formatCompactJsonBlock("### Response headers", latestInteraction.response.responseHeaders)); + } + + lines.push("### Context messages sent to model"); + latestInteraction.context.messages.forEach((message, index) => { + lines.push(`- #${index + 1} role=${message.role} len=${message.contentLength}`); + lines.push(` preview=${truncate(message.contentPreview, 220)}`); + }); + lines.push(""); + + lines.push("### Parsed result"); + lines.push(`Visible content:\n${latestInteraction.parsed.visibleContent || "(empty)"}`); + lines.push(""); + if (latestInteraction.parsed.reasoningContent.trim()) { + lines.push(`Thinking / reasoning:\n${truncate(latestInteraction.parsed.reasoningContent, 4000)}`); + lines.push(""); + } + lines.push(`Raw content:\n${truncate(latestInteraction.parsed.rawContent || "(empty)", 3000)}`); + lines.push(""); + + const chunkSummary = summarizeStreamChunks(latestInteraction); + if (chunkSummary.length > 0) { + lines.push("### Stream summary"); + chunkSummary.forEach((chunk) => { + lines.push(`- ${chunk.label}: ${chunk.value}`); + }); + lines.push(""); + } + + const sanitizationSummary = summarizeSanitization(latestInteraction); + if (sanitizationSummary.length > 0) { + lines.push("### Sanitization summary"); + sanitizationSummary.forEach((event) => { + lines.push(`- ${event.stage}: ${event.count} change(s)${event.repairedEncoding ? " with encoding repair" : ""}`); + }); + lines.push(""); + } + + if (latestInteraction.dbWrites.length > 0) { + lines.push(...formatJsonBlock("### DB writes", latestInteraction.dbWrites)); + } + + if (latestInteraction.errors.length > 0) { + lines.push(...formatJsonBlock("### Errors", latestInteraction.errors)); + } + + lines.push("### Timeline"); + latestInteraction.timeline.forEach((step) => { + lines.push(`- ${step.timestamp} [${step.phase}] ${step.detail}`); + }); + lines.push(""); + } + + lines.push("## Persisted Messages"); for (const message of messages) { lines.push(`[${message.role.toUpperCase()}] #${message.id}`); lines.push(`Created: ${formatTimestamp(message.created_at)}`);