feat: context compaction button in token bar (≥50% usage)
This commit is contained in:
17
src/App.tsx
17
src/App.tsx
@@ -43,6 +43,21 @@ export default function App() {
|
||||
setSplitSession(prev => prev === key ? null : key);
|
||||
}, []);
|
||||
|
||||
const handleCompact = useCallback(async (key: string): Promise<boolean> => {
|
||||
const client = getClient();
|
||||
if (!client) return false;
|
||||
try {
|
||||
const res = await client.send('sessions.compact', { key });
|
||||
// Reload history after compaction to reflect new state
|
||||
if (res?.compacted) {
|
||||
switchSession(key);
|
||||
}
|
||||
return !!res?.compacted;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, [getClient, switchSession]);
|
||||
|
||||
// Split pane drag
|
||||
useEffect(() => {
|
||||
if (!splitDragging) return;
|
||||
@@ -152,7 +167,7 @@ export default function App() {
|
||||
<div ref={splitContainerRef} className="flex-1 flex min-w-0" aria-hidden={sidebarOpen ? true : undefined}>
|
||||
{/* Primary pane */}
|
||||
<main className="flex flex-col min-w-0" style={splitSession ? { width: `${splitRatio}%` } : { flex: 1 }} aria-label={t('app.mainChat')}>
|
||||
<Header status={status} sessionKey={activeSession} onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} soundEnabled={soundEnabled} onToggleSound={toggleSound} messages={messages} agentAvatarUrl={agentIdentity?.avatar} agentName={agentIdentity?.name} />
|
||||
<Header status={status} sessionKey={activeSession} onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} soundEnabled={soundEnabled} onToggleSound={toggleSound} messages={messages} agentAvatarUrl={agentIdentity?.avatar} agentName={agentIdentity?.name} onCompact={handleCompact} />
|
||||
<ConnectionBanner status={status} />
|
||||
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-pc-text-muted"><div className="animate-pulse text-sm">Loading…</div></div>}>
|
||||
<Chat messages={messages} isGenerating={isGenerating} isLoadingHistory={isLoadingHistory} status={status} sessionKey={activeSession} onSend={sendMessage} onAbort={abort} agentAvatarUrl={agentIdentity?.avatar} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Menu, Sparkles, LogOut, Volume2, VolumeOff, Cpu, Bot, Download } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Menu, Sparkles, LogOut, Volume2, VolumeOff, Cpu, Bot, Download, Minimize2 } from 'lucide-react';
|
||||
import type { ConnectionStatus, Session, ChatMessage } from '../types';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
import { LanguageSelector } from './LanguageSelector';
|
||||
@@ -18,9 +18,10 @@ interface Props {
|
||||
messages?: ChatMessage[];
|
||||
agentAvatarUrl?: string;
|
||||
agentName?: string;
|
||||
onCompact?: (sessionKey: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages, agentAvatarUrl, agentName }: Props) {
|
||||
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages, agentAvatarUrl, agentName, onCompact }: Props) {
|
||||
const t = useT();
|
||||
const sessionLabel = activeSessionData ? sessionDisplayName(activeSessionData) : (sessionKey.split(':').pop() || sessionKey);
|
||||
|
||||
@@ -129,9 +130,39 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
||||
<span className="text-[11px] text-pc-text-secondary tabular-nums shrink-0 whitespace-nowrap">
|
||||
{(total / 1000).toFixed(1)}k / {(ctx / 1000).toFixed(0)}k tokens
|
||||
</span>
|
||||
{onCompact && pct >= 50 && (
|
||||
<CompactButton sessionKey={sessionKey} onCompact={onCompact} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactButton({ sessionKey, onCompact }: { sessionKey: string; onCompact: (key: string) => Promise<boolean> }) {
|
||||
const [compacting, setCompacting] = useState(false);
|
||||
const t = useT();
|
||||
|
||||
const handleCompact = useCallback(async () => {
|
||||
if (compacting) return;
|
||||
setCompacting(true);
|
||||
try {
|
||||
await onCompact(sessionKey);
|
||||
} finally {
|
||||
setCompacting(false);
|
||||
}
|
||||
}, [compacting, sessionKey, onCompact]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCompact}
|
||||
disabled={compacting}
|
||||
className="inline-flex items-center gap-1 text-[10px] text-pc-text-muted hover:text-pc-text shrink-0 px-1.5 py-0.5 rounded hover:bg-[var(--pc-hover)] transition-colors disabled:opacity-50"
|
||||
title={t('header.compact')}
|
||||
>
|
||||
<Minimize2 className={`h-3 w-3 ${compacting ? 'animate-pulse' : ''}`} />
|
||||
<span className="hidden sm:inline">{compacting ? t('header.compacting') : t('header.compact')}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ const en = {
|
||||
|
||||
// Export
|
||||
'header.export': 'Export conversation as Markdown',
|
||||
'header.compact': 'Compact',
|
||||
'header.compacting': 'Compacting…',
|
||||
|
||||
// Theme
|
||||
'theme.title': 'Theme',
|
||||
@@ -253,6 +255,8 @@ const fr: Record<keyof typeof en, string> = {
|
||||
'shortcuts.generalSection': 'Général',
|
||||
|
||||
'header.export': 'Exporter la conversation en Markdown',
|
||||
'header.compact': 'Compacter',
|
||||
'header.compacting': 'Compaction…',
|
||||
|
||||
'theme.title': 'Thème',
|
||||
'theme.mode': 'Mode',
|
||||
|
||||
Reference in New Issue
Block a user