From 99aff0be6fe60bffe19a07a04a94677b4fb72bbe Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Fri, 3 Apr 2026 23:51:39 -0300 Subject: [PATCH] feat(ui): include runtimeLastError in debug transcript Passes the last_error.message from the runtime snapshot down through App to ChatView and into the chat debug formatter. This ensures that any specific errors that occur during local API startup, model loading, or operation are captured directly in the debug export. --- tauri-app/src/App.tsx | 185 +++++++++++++++++++------- tauri-app/src/components/ChatView.tsx | 13 +- tauri-app/src/lib/chatDebug.ts | 9 +- 3 files changed, 149 insertions(+), 58 deletions(-) diff --git a/tauri-app/src/App.tsx b/tauri-app/src/App.tsx index 22500a5..3285efd 100644 --- a/tauri-app/src/App.tsx +++ b/tauri-app/src/App.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import Sidebar from "./components/Sidebar"; import ChatView from "./components/ChatView"; +import AppHeader from "./components/AppHeader"; import SettingsPage from "./features/settings/SettingsPage"; import { ToastProvider } from "./components/Toast"; import { useDB } from "./hooks/useDB"; @@ -8,19 +9,60 @@ import { useModels } from "./hooks/useModels"; import { useRuntimeManager } from "./features/runtime/useRuntimeManager"; import { useRuntimeReadyModelRefresh } from "./features/runtime/useRuntimeReadyModelRefresh"; import { swapModel } from "./features/runtime/api"; -import type { Chat } from "./lib/types"; +import type { Chat, ChatTokenTotals } from "./lib/types"; import type { RuntimeModelFile } from "./features/runtime/types"; import "./App.css"; +const SIDEBAR_STORAGE_KEY = "forgelm.sidebar.open"; + +function readStoredSidebarOpen(): boolean { + if (typeof window === "undefined") return true; + + const getItem = window.localStorage?.getItem; + if (typeof getItem !== "function") return true; + + try { + const saved = getItem.call(window.localStorage, SIDEBAR_STORAGE_KEY); + return saved === null ? true : saved === "true"; + } catch { + return true; + } +} + +function persistSidebarOpen(value: boolean): void { + if (typeof window === "undefined") return; + + const setItem = window.localStorage?.setItem; + if (typeof setItem !== "function") return; + + try { + setItem.call(window.localStorage, SIDEBAR_STORAGE_KEY, String(value)); + } catch { + // Ignore storage failures and keep the in-memory UI state functional. + } +} + export default function App() { const { db, ready } = useDB(); const [currentChatId, setCurrentChatId] = useState(null); const [chats, setChats] = useState([]); - const [sidebarOpen, setSidebarOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(readStoredSidebarOpen); const [view, setView] = useState<"chat" | "settings">("chat"); + // Header live-state (lifted from ChatView via callbacks) + const [generating, setGenerating] = useState(false); + const [streamTokenCount, setStreamTokenCount] = useState(0); + const [tps, setTps] = useState(null); + const [chatTokenTotals, setChatTokenTotals] = useState({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }); + const runtime = useRuntimeManager(); - const { models, selectedModel, setSelectedModel, status, refresh: refreshModels } = useModels( + const { + models, + selectedModel, + setSelectedModel, + status, + refresh: refreshModels, + } = useModels( runtime.connection.serverUrl, runtime.connection.apiKey, Boolean(runtime.connection.serverUrl), @@ -36,12 +78,15 @@ export default function App() { useEffect(() => { loadChats(); }, [loadChats]); + useEffect(() => { + persistSidebarOpen(sidebarOpen); + }, [sidebarOpen]); + const newChat = useCallback(() => { if (!db) return; const id = db.insert("INSERT INTO chats (title) VALUES ('New Chat')"); if (id > 0) { setCurrentChatId(id); - setSidebarOpen(false); loadChats(); } }, [db, loadChats]); @@ -51,13 +96,11 @@ export default function App() { if (!window.confirm("Delete this conversation and all of its messages?")) return; db.run("DELETE FROM chats WHERE id = ?", [id]); if (currentChatId === id) setCurrentChatId(null); - setSidebarOpen(false); loadChats(); }, [db, currentChatId, loadChats]); const handleSelectChat = useCallback((id: number) => { setCurrentChatId(id); - setSidebarOpen(false); }, []); const [isSwapping, setIsSwapping] = useState(false); @@ -77,9 +120,13 @@ export default function App() { } }, [setSelectedModel]); - const localModels: RuntimeModelFile[] = useMemo(() => { - return runtime.scannedModels ?? []; - }, [runtime.scannedModels]); + const localModels: RuntimeModelFile[] = useMemo(() => runtime.scannedModels ?? [], [runtime.scannedModels]); + + // Current chat title for breadcrumb + const currentChatTitle = useMemo( + () => chats.find((c) => c.id === currentChatId)?.title ?? undefined, + [chats, currentChatId], + ); if (!ready) { return ( @@ -92,56 +139,92 @@ export default function App() { ); } + const serverConnected = Boolean(runtime.connection.serverUrl); + return ( -
- {view !== "settings" && ( - + + {/* ── Global header — always visible (settings mode shows back button) ── */} + setSidebarOpen((v) => !v)} onOpenSettings={() => setView("settings")} - onCloseMobile={() => setSidebarOpen(false)} + onBackToChat={view === "settings" ? () => setView("chat") : undefined} + models={models} + localModels={localModels} + selectedModel={selectedModel} + onModelChange={setSelectedModel} + onModelSwap={handleModelSwap} + isSwapping={isSwapping} + status={status} + runtimeStatus={runtime.snapshot.status} + serverConnected={serverConnected} + generating={generating} + streamTokenCount={streamTokenCount} + tps={tps} + chatTitle={view === "settings" ? "Settings" : currentChatTitle} + chatTokenTotals={chatTokenTotals} /> - )} - {view !== "settings" && sidebarOpen && ( -
-
); } diff --git a/tauri-app/src/components/ChatView.tsx b/tauri-app/src/components/ChatView.tsx index 55a3050..9cd20e2 100644 --- a/tauri-app/src/components/ChatView.tsx +++ b/tauri-app/src/components/ChatView.tsx @@ -60,6 +60,7 @@ interface Props { status: ServerStatus; runtimeMode?: "local" | "remote"; runtimeStatus?: RuntimeStatus; + runtimeLastError?: string; showDebugTools?: boolean; onChatCreated: (id: number) => void; onChatUpdated: () => void; @@ -74,16 +75,15 @@ interface Props { /** Lift TPS to parent for header display */ onTpsChange?: (v: number | null) => void; onChatTokenTotalsChange?: (totals: ChatTokenTotals) => void; -} + } -export default function ChatView({ - db, chatId, server, apiKey, model, + export default function ChatView({ + db, chatId, server, apiKey, model, models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status, - runtimeMode = "remote", runtimeStatus = "ready", showDebugTools = Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV), + runtimeMode = "remote", runtimeStatus = "ready", runtimeLastError, showDebugTools = Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV), onChatCreated, onChatUpdated, onNewChat, onOpenSidebar, onOpenSettings, onDegradationDetected, onGeneratingChange, onStreamTokenCountChange, onTpsChange, onChatTokenTotalsChange, -}: Props) { - const [messages, setMessages] = useState([]); + }: Props) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [generating, setGenerating] = useState(false); const [streamContent, setStreamContent] = useState(""); @@ -727,6 +727,7 @@ export default function ChatView({ latestInteraction, runtimeMode, runtimeStatus, + runtimeLastError, }); await navigator.clipboard.writeText(transcript); setDebugCopied(true); diff --git a/tauri-app/src/lib/chatDebug.ts b/tauri-app/src/lib/chatDebug.ts index 96452ef..e316381 100644 --- a/tauri-app/src/lib/chatDebug.ts +++ b/tauri-app/src/lib/chatDebug.ts @@ -14,6 +14,7 @@ interface FormatChatDebugTranscriptOptions { latestInteraction?: InteractionLog | null; runtimeMode?: "local" | "remote"; runtimeStatus?: string; + runtimeLastError?: string; } function formatTimestamp(timestamp: number | null | undefined): string { @@ -113,6 +114,7 @@ export function formatChatDebugTranscript({ latestInteraction, runtimeMode, runtimeStatus, + runtimeLastError, }: FormatChatDebugTranscriptOptions): string { const lines = [ "# Chat Debug Export", @@ -127,9 +129,14 @@ export function formatChatDebugTranscript({ `Agentic mode: ${agenticMode ? "on" : "off"}`, `Runtime mode: ${runtimeMode ?? "unknown"}`, `Runtime status: ${runtimeStatus ?? "unknown"}`, - "", ]; + if (runtimeLastError) { + lines.push(`Runtime error: ${runtimeLastError}`); + } + + lines.push(""); + if (savedMemories.length > 0) { lines.push("## Saved Memories Considered"); for (const memory of savedMemories) {