diff --git a/packages/gateway/src/routes/chat-history.ts b/packages/gateway/src/routes/chat-history.ts
index 52fbfe6d..5f5b7c3c 100644
--- a/packages/gateway/src/routes/chat-history.ts
+++ b/packages/gateway/src/routes/chat-history.ts
@@ -44,6 +44,21 @@ import type { ChannelIncomingMessage } from '@ownpilot/core';
const log = getLog('ChatHistory');
+function normalizeTrace(trace: any): any {
+ if (!trace) return trace;
+ return {
+ ...trace,
+ modelCalls: trace.modelCalls || [],
+ autonomyChecks: trace.autonomyChecks || [],
+ dbOperations: trace.dbOperations || { reads: 0, writes: 0 },
+ memoryOps: trace.memoryOps || { adds: 0, recalls: 0 },
+ triggersFired: trace.triggersFired || [],
+ errors: trace.errors || [],
+ events: trace.events || [],
+ toolCalls: trace.toolCalls || [],
+ };
+}
+
export const chatHistoryRoutes = new Hono();
// =====================================================
@@ -254,7 +269,7 @@ chatHistoryRoutes.get('/history/:id', async (c) => {
provider: msg.provider,
model: msg.model,
toolCalls: msg.toolCalls,
- trace: msg.trace,
+ trace: normalizeTrace(msg.trace),
isError: msg.isError,
createdAt: msg.createdAt.toISOString(),
})),
@@ -315,7 +330,7 @@ chatHistoryRoutes.get('/history/:id/unified', async (c) => {
provider: msg.provider,
model: msg.model,
toolCalls: msg.toolCalls,
- trace: msg.trace,
+ trace: normalizeTrace(msg.trace),
isError: msg.isError,
createdAt: msg.createdAt.toISOString(),
source: 'web' as const,
@@ -400,7 +415,7 @@ chatHistoryRoutes.get('/history/:id/unified', async (c) => {
provider: msg.provider,
model: msg.model,
toolCalls: msg.toolCalls,
- trace: msg.trace,
+ trace: normalizeTrace(msg.trace),
isError: msg.isError,
createdAt: msg.createdAt.toISOString(),
source: 'ai',
diff --git a/packages/gateway/src/services/conversation-service.ts b/packages/gateway/src/services/conversation-service.ts
index d4f97dae..2d4580b4 100644
--- a/packages/gateway/src/services/conversation-service.ts
+++ b/packages/gateway/src/services/conversation-service.ts
@@ -235,6 +235,11 @@ export class ConversationService {
},
]
: [],
+ autonomyChecks: [],
+ dbOperations: { reads: 0, writes: 0 },
+ memoryOps: { adds: 0, recalls: 0 },
+ triggersFired: [],
+ errors: [],
mcpToolEvents,
events: mcpToolEvents.map((event) => ({
type: event.type,
diff --git a/packages/ui/src/components/Sidebar.tsx b/packages/ui/src/components/Sidebar.tsx
index 6beae986..8591b5f0 100644
--- a/packages/ui/src/components/Sidebar.tsx
+++ b/packages/ui/src/components/Sidebar.tsx
@@ -19,7 +19,7 @@ import { SidebarFooter } from './sidebar/SidebarFooter';
import { SidebarDataSection } from './sidebar/SidebarDataSection';
import { useToast } from './ToastProvider';
import { useDialog } from './ConfirmDialog';
-import { X, ChevronRight, Search, Calendar, Edit2, Trash2, Globe, MessageSquare, Telegram, WhatsApp } from './icons';
+import { X, ChevronRight, Search, Calendar, Edit2, Trash2, Globe, MessageSquare, Telegram, WhatsApp, Settings2 } from './icons';
import type { NavItem } from '../constants/nav-items';
import type { Conversation } from '../api/types';
import { chatApi } from '../api/endpoints/chat';
@@ -56,7 +56,7 @@ function PinnedNavLink({ item, badge, onCloseCustomize, isCustomizeOpen }: { ite
clearMessages();
navigate('/', { replace: true });
// Reset backend context (best-effort)
- chatApi.resetContext(provider, model).catch(() => {});
+ chatApi.resetContext(provider, model).catch(() => { });
}
};
@@ -72,10 +72,9 @@ function PinnedNavLink({ item, badge, onCloseCustomize, isCustomizeOpen }: { ite
end={item.to === '/'}
onClick={handleClick}
className={({ isActive }) =>
- `flex items-center gap-2 px-3 py-2.5 md:py-1.5 rounded-md transition-all text-base ${
- isActive && !isCustomizeOpen && !hasActiveConversation
- ? 'bg-primary/10 text-primary border-l-[3px] border-primary'
- : 'text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:translate-x-0.5'
+ `flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors text-sm font-medium ${isActive && !isCustomizeOpen && !hasActiveConversation
+ ? 'bg-primary/10 text-primary dark:bg-primary/20'
+ : 'text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:text-text-primary dark:hover:text-dark-text-primary'
}`
}
>
@@ -203,9 +202,8 @@ export function Sidebar({ isMobile, isOpen, onClose, onSearchOpen, onCustomizeTo
data-testid="sidebar"
className={
isMobile
- ? `fixed inset-y-0 left-0 z-40 w-64 bg-bg-secondary dark:bg-dark-bg-secondary flex flex-col transform transition-transform duration-200 ease-out ${
- isOpen ? 'translate-x-0' : '-translate-x-full'
- }`
+ ? `fixed inset-y-0 left-0 z-40 w-64 bg-bg-secondary dark:bg-dark-bg-secondary flex flex-col transform transition-transform duration-200 ease-out ${isOpen ? 'translate-x-0' : '-translate-x-full'
+ }`
: `${desktopWidthClass} border-r border-border dark:border-dark-border bg-bg-secondary dark:bg-dark-bg-secondary flex flex-col`
}
>
@@ -277,7 +275,7 @@ export function Sidebar({ isMobile, isOpen, onClose, onSearchOpen, onCustomizeTo
key="search"
onClick={onSearchOpen}
data-testid="sidebar-search-btn"
- className="w-full flex items-center gap-2 px-3 py-2.5 md:py-1.5 rounded-md transition-all text-base text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:translate-x-0.5 text-left"
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors text-sm font-medium text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:text-text-primary dark:hover:text-dark-text-primary text-left"
>
Search
@@ -293,10 +291,9 @@ export function Sidebar({ isMobile, isOpen, onClose, onSearchOpen, onCustomizeTo
onClick={onCloseCustomize}
data-testid="sidebar-scheduled-link"
className={({ isActive }) =>
- `flex items-center gap-2 px-3 py-2.5 md:py-1.5 rounded-md transition-all text-base ${
- isActive && !isCustomizeOpen
- ? 'bg-primary/10 text-primary border-l-[3px] border-primary'
- : 'text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:translate-x-0.5'
+ `flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors text-sm font-medium ${isActive && !isCustomizeOpen
+ ? 'bg-primary/10 text-primary dark:bg-primary/20'
+ : 'text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:text-text-primary dark:hover:text-dark-text-primary'
}`
}
>
@@ -310,13 +307,12 @@ export function Sidebar({ isMobile, isOpen, onClose, onSearchOpen, onCustomizeTo
-
+
Customize
@@ -330,7 +326,7 @@ export function Sidebar({ isMobile, isOpen, onClose, onSearchOpen, onCustomizeTo
{ onCloseCustomize(); navigate('/history'); }}
data-testid="sidebar-recents"
- className="w-full flex items-center gap-2 px-3 py-2.5 md:py-1.5 rounded-md transition-all text-base text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:translate-x-0.5 text-left"
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors text-sm font-medium text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:text-text-primary dark:hover:text-dark-text-primary text-left"
>
Recent
@@ -352,124 +348,124 @@ export function Sidebar({ isMobile, isOpen, onClose, onSearchOpen, onCustomizeTo
{ onCloseCustomize(); navigate('/history'); }}
- className="flex-1 text-left text-[15px] font-semibold text-text-muted dark:text-dark-text-muted uppercase tracking-wider hover:text-text-secondary dark:hover:text-dark-text-secondary transition-colors"
+ className="flex-1 text-left text-[11px] font-semibold text-text-muted dark:text-dark-text-muted uppercase tracking-wider hover:text-text-secondary dark:hover:text-dark-text-secondary transition-colors"
>
Recent
- {!collapsed.recents && <>
- recents.setSearch(e.target.value)}
- placeholder="Search\u2026"
- data-testid="sidebar-recents-search"
- className="w-full px-2 py-1 text-xs rounded border border-border dark:border-dark-border bg-bg-primary dark:bg-dark-bg-primary text-text-primary dark:text-dark-text-primary placeholder:text-text-muted focus:outline-none focus:border-primary"
- />
-
- {recents.availablePlatforms.size > 0 && (
-
- {(['all', 'web', ...(recents.availablePlatforms.has('whatsapp') ? ['whatsapp'] : []), ...(recents.availablePlatforms.has('telegram') ? ['telegram'] : [])] as SourceFilter[]).map((tab) => (
- recents.setSourceFilter(tab)}
- className={`flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium whitespace-nowrap transition-colors ${
- recents.sourceFilter === tab
- ? 'bg-primary text-white'
- : 'text-text-muted dark:text-dark-text-muted hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary'
- }`}
- >
- {tab === 'all' && 'All'}
- {tab === 'web' && <> Web>}
- {tab === 'whatsapp' && <> WA>}
- {tab === 'telegram' && <> TG>}
-
- ))}
-
- )}
- {recents.isLoading && recents.conversations.length === 0 ? (
-
- {[...Array(4)].map((_, i) => (
-
- ))}
+ {!collapsed.recents && <>
+
+
+ recents.setSearch(e.target.value)}
+ placeholder="Search..."
+ data-testid="sidebar-recents-search"
+ className="w-full pl-8 pr-3 py-1.5 text-[13px] rounded-md bg-black/5 dark:bg-white/5 border border-transparent text-text-primary dark:text-dark-text-primary placeholder:text-text-muted focus:outline-none focus:border-primary/50 focus:bg-transparent dark:focus:bg-transparent focus:ring-1 focus:ring-primary/50 transition-all"
+ />
- ) : recents.conversations.length === 0 ? (
-
- {recents.search ? 'No results' : 'No conversations yet'}
-
- ) : (
- <>
- {/* Optimistic entries: one per active session, shows immediately
+
+ {recents.availablePlatforms.size > 0 && (
+
+ {(['all', 'web', ...(recents.availablePlatforms.has('whatsapp') ? ['whatsapp'] : []), ...(recents.availablePlatforms.has('telegram') ? ['telegram'] : [])] as SourceFilter[]).map((tab) => (
+ recents.setSourceFilter(tab)}
+ className={`flex items-center gap-1.5 px-2 py-0.5 rounded text-[11px] font-medium whitespace-nowrap transition-colors ${recents.sourceFilter === tab
+ ? 'bg-primary text-white'
+ : 'text-text-muted dark:text-dark-text-muted hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary'
+ }`}
+ >
+ {tab === 'all' && 'All'}
+ {tab === 'web' && <> Web>}
+ {tab === 'whatsapp' && <> WA>}
+ {tab === 'telegram' && <> TG>}
+
+ ))}
+
+ )}
+ {recents.isLoading && recents.conversations.length === 0 ? (
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+ ) : recents.conversations.length === 0 ? (
+
+ {recents.search ? 'No results' : 'No conversations yet'}
+
+ ) : (
+ <>
+ {/* Optimistic entries: one per active session, shows immediately
when user sends a message. Each persists in sidebar until
the DB row arrives via WS chat:history:updated. */}
- {optimisticEntries.map((entry) => {
- const isActive = activeConversationId === entry.id;
- return (
-
-
handleRecentClick(entry.id)}
- className={`group relative flex items-center gap-1.5 px-2 py-1.5 mx-1 my-0.5 rounded-md cursor-pointer transition-colors ${
- isActive ? 'bg-primary/10 text-primary' : 'hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary text-text-secondary dark:text-dark-text-secondary'
- }`}
- >
-
- {entry.title}
-
-
- );
- })}
- {recents.groups.map((group) => (
-
-
{group.label}
- {group.items.map((conv) => {
- const isActiveConv = activeConversationId === conv.id;
- const isEditing = recents.editingId === conv.id;
- const title = getConvTitle(conv);
- const isChannel = conv.source === 'channel';
- const isTelegram = conv.channelPlatform === 'telegram';
+ {optimisticEntries.map((entry) => {
+ const isActive = activeConversationId === entry.id;
return (
-
handleRecentClick(conv.id)}
- className={`group relative flex items-center gap-1.5 px-2 py-1.5 mx-1 my-0.5 rounded-md cursor-pointer transition-colors ${
- isActiveConv ? 'bg-primary/10 text-primary' : 'hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary text-text-secondary dark:text-dark-text-secondary'
- }`}
- >
- {isChannel && isTelegram ?
: isChannel && conv.channelPlatform === 'whatsapp' ?
: isChannel ?
:
}
- {isEditing ? (
-
recents.setEditTitle(e.target.value)}
- onBlur={() => handleCommitEdit(conv.id)}
- onKeyDown={(e) => { if (e.key === 'Enter') handleCommitEdit(conv.id); if (e.key === 'Escape') recents.cancelEdit(); }}
- onClick={(e) => e.stopPropagation()}
- className="flex-1 min-w-0 text-xs bg-bg-primary dark:bg-dark-bg-primary border border-primary rounded px-1 py-0.5 outline-none"
- autoFocus
- />
- ) : (
-
{title}
- )}
- {!isEditing && (
-
- handleStartEdit(conv, e)} title="Rename" className="p-0.5 rounded transition-colors hover:text-text-primary dark:hover:text-dark-text-primary">
- handleDeleteConv(conv.id, e)} title="Delete" className="p-0.5 rounded hover:text-error transition-colors">
-
- )}
+
+
handleRecentClick(entry.id)}
+ className={`group relative flex items-center gap-2 pl-7 pr-2 py-1.5 mx-2 my-0.5 rounded-md cursor-pointer transition-colors ${isActive ? 'bg-primary/10 text-primary dark:bg-primary/20 font-medium' : 'hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary text-text-secondary dark:text-dark-text-secondary hover:text-text-primary dark:hover:text-dark-text-primary text-[13px]'
+ }`}
+ >
+
+ {entry.title}
+
);
})}
-
- ))}
- >
- )}
- {recents.total > recents.conversations.length && (
-
+{recents.total - recents.conversations.length} older
- )}
-
- All conversations →
-
+ {recents.groups.map((group) => (
+
+
{group.label}
+ {group.items.map((conv) => {
+ const isActiveConv = activeConversationId === conv.id;
+ const isEditing = recents.editingId === conv.id;
+ const title = getConvTitle(conv);
+ const isChannel = conv.source === 'channel';
+ const isTelegram = conv.channelPlatform === 'telegram';
+ return (
+
handleRecentClick(conv.id)}
+ className={`group relative flex items-center gap-2 pl-7 pr-2 py-1.5 mx-2 my-0.5 rounded-md cursor-pointer transition-colors ${isActiveConv ? 'bg-primary/10 text-primary dark:bg-primary/20 font-medium' : 'hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary text-text-secondary dark:text-dark-text-secondary hover:text-text-primary dark:hover:text-dark-text-primary text-[13px]'
+ }`}
+ >
+ {isChannel && isTelegram ?
: isChannel && conv.channelPlatform === 'whatsapp' ?
: isChannel ?
:
}
+ {isEditing ? (
+
recents.setEditTitle(e.target.value)}
+ onBlur={() => handleCommitEdit(conv.id)}
+ onKeyDown={(e) => { if (e.key === 'Enter') handleCommitEdit(conv.id); if (e.key === 'Escape') recents.cancelEdit(); }}
+ onClick={(e) => e.stopPropagation()}
+ className="flex-1 min-w-0 text-xs bg-bg-primary dark:bg-dark-bg-primary border border-primary rounded px-1 py-0.5 outline-none"
+ autoFocus
+ />
+ ) : (
+
{title}
+ )}
+ {!isEditing && (
+
+ handleStartEdit(conv, e)} title="Rename" className="p-0.5 rounded transition-colors hover:text-text-primary dark:hover:text-dark-text-primary">
+ handleDeleteConv(conv.id, e)} title="Delete" className="p-0.5 rounded hover:text-error transition-colors">
+
+ )}
+
+ );
+ })}
+
+ ))}
+ >
+ )}
+ {recents.total > recents.conversations.length && (
+
+{recents.total - recents.conversations.length} older
+ )}
+
+ All conversations →
+
>}
diff --git a/packages/ui/src/components/sidebar/SidebarDataSection.tsx b/packages/ui/src/components/sidebar/SidebarDataSection.tsx
index 304eb04e..c98fc1e2 100644
--- a/packages/ui/src/components/sidebar/SidebarDataSection.tsx
+++ b/packages/ui/src/components/sidebar/SidebarDataSection.tsx
@@ -43,7 +43,7 @@ export function SidebarDataSection({
{ onCloseCustomize(); navigate(def.route); }}
data-testid={`sidebar-${config.id}`}
- className="w-full flex items-center gap-2 px-3 py-2.5 md:py-1.5 rounded-md transition-all text-base text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:translate-x-0.5 text-left"
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors text-sm font-medium text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:text-text-primary dark:hover:text-dark-text-primary text-left"
>
{label}
@@ -64,7 +64,7 @@ export function SidebarDataSection({
{ onCloseCustomize(); navigate(def.route); }}
- className="flex-1 text-left text-[15px] font-semibold text-text-muted dark:text-dark-text-muted uppercase tracking-wider hover:text-text-secondary dark:hover:text-dark-text-secondary transition-colors"
+ className="flex-1 text-left text-[11px] font-semibold text-text-muted dark:text-dark-text-muted uppercase tracking-wider hover:text-text-secondary dark:hover:text-dark-text-secondary transition-colors"
>
{label}
@@ -84,12 +84,12 @@ export function SidebarDataSection({
) : items.length === 0 ? (
No {label.toLowerCase()}
) : (
-
+
{items.map((item) => (
{ onCloseCustomize(); navigate(item.route); }}
- className="w-full flex items-center gap-2 px-3 py-2.5 md:py-1.5 rounded-md transition-all text-base text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:translate-x-0.5 text-left"
+ className="w-full flex items-center gap-2 pl-7 pr-2 py-1.5 rounded-md transition-colors text-[13px] text-text-secondary dark:text-dark-text-secondary hover:bg-bg-tertiary dark:hover:bg-dark-bg-tertiary hover:text-text-primary dark:hover:text-dark-text-primary text-left"
title={item.label}
>