fix: improve Mini Chat session controls
Removed duplicate Mini Chat launch action from the closed-sidebar header Enabled opening Mini Chat from draft sessions via the header PiP action Added read-only context usage info to existing Mini Chat sessions
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip';
|
||||
|
||||
import { RiArrowLeftSLine, RiChat4Line, RiChatNewLine, RiCheckLine, RiCloseLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiLayoutRightLine, RiPictureInPicture2Line, RiPlayListAddLine, RiRefreshLine, RiServerLine, RiStackLine, RiTerminalBoxLine, RiTimerLine, RiAlertLine, RiWindowLine, type RemixiconComponentType } from '@remixicon/react';
|
||||
import { RiArrowLeftSLine, RiChat4Line, RiChatNewLine, RiCheckLine, RiCloseLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiLayoutRightLine, RiPictureInPicture2Line, RiPlayListAddLine, RiRefreshLine, RiServerLine, RiStackLine, RiTerminalBoxLine, RiTimerLine, RiAlertLine, type RemixiconComponentType } from '@remixicon/react';
|
||||
import { DiffIcon } from '@/components/icons/DiffIcon';
|
||||
import { useUIStore, type MainTab } from '@/stores/useUIStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
@@ -1289,7 +1289,12 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
});
|
||||
}, [activeProject?.id, activeProject?.path, openDirectory]);
|
||||
|
||||
const handleOpenCurrentSessionMiniChat = React.useCallback(() => {
|
||||
const handleOpenCurrentMiniChat = React.useCallback(() => {
|
||||
if (isNewSessionDraftOpen) {
|
||||
handleOpenDraftMiniChat();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSessionId) {
|
||||
return;
|
||||
}
|
||||
@@ -1299,7 +1304,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
}).catch((error) => {
|
||||
console.warn('[header] failed to open session mini chat window', error);
|
||||
});
|
||||
}, [activeProject?.path, currentSessionId, openDirectory]);
|
||||
}, [activeProject?.path, currentSessionId, handleOpenDraftMiniChat, isNewSessionDraftOpen, openDirectory]);
|
||||
|
||||
const handleOpenContextPanel = React.useCallback(() => {
|
||||
const directory = normalize(openDirectory || '');
|
||||
@@ -1823,6 +1828,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
);
|
||||
|
||||
const desktopSidebarActionsInline = !isRightSidebarOpen || !desktopRightSidebarActionsHost;
|
||||
const showMiniChatHeaderAction = hasElectronDesktopIPC && (isNewSessionDraftOpen || Boolean(currentSessionId));
|
||||
|
||||
const renderDesktop = () => (
|
||||
<div
|
||||
@@ -1863,23 +1869,6 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{hasElectronDesktopIPC && !isLeftSidebarOpen ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('header.actions.newMiniChatAria')}
|
||||
onClick={handleOpenDraftMiniChat}
|
||||
className={cn(desktopHeaderIconButtonClass, 'mr-6 shrink-0')}
|
||||
>
|
||||
<RiWindowLine className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('header.actions.newMiniChat')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{projectActionsContext && (
|
||||
<ProjectActionsButton
|
||||
projectRef={projectActionsContext.projectRef}
|
||||
@@ -1931,14 +1920,6 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<HeaderIconActionButton
|
||||
visible={hasElectronDesktopIPC && !isNewSessionDraftOpen && Boolean(currentSessionId)}
|
||||
title={t('header.actions.openSessionMiniChat')}
|
||||
ariaLabel={t('header.actions.openSessionMiniChatAria')}
|
||||
onClick={handleOpenCurrentSessionMiniChat}
|
||||
className={`${desktopHeaderIconButtonClass} mr-1`}
|
||||
Icon={RiPictureInPicture2Line}
|
||||
/>
|
||||
{showDesktopHeaderContextUsage && stableDesktopContextUsage ? (
|
||||
<ContextUsageDisplay
|
||||
totalTokens={stableDesktopContextUsage.totalTokens}
|
||||
@@ -1951,11 +1932,19 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
showPercentIcon
|
||||
onClick={handleOpenContextPanel}
|
||||
pressed={isContextPanelActive}
|
||||
className={desktopSidebarActionsInline ? 'mr-3.5' : ''}
|
||||
className={desktopSidebarActionsInline && !showMiniChatHeaderAction ? 'mr-3.5' : ''}
|
||||
valueClassName="typography-ui-label font-medium leading-none text-foreground"
|
||||
percentIconClassName="h-5 w-5"
|
||||
/>
|
||||
) : null}
|
||||
<HeaderIconActionButton
|
||||
visible={showMiniChatHeaderAction}
|
||||
title={isNewSessionDraftOpen ? t('header.actions.newMiniChat') : t('header.actions.openSessionMiniChat')}
|
||||
ariaLabel={isNewSessionDraftOpen ? t('header.actions.newMiniChatAria') : t('header.actions.openSessionMiniChatAria')}
|
||||
onClick={handleOpenCurrentMiniChat}
|
||||
className={cn(desktopHeaderIconButtonClass, desktopSidebarActionsInline && showDesktopHeaderContextUsage ? 'mr-3.5' : 'mr-1')}
|
||||
Icon={RiPictureInPicture2Line}
|
||||
/>
|
||||
{desktopSidebarActionsInline ? desktopSidebarActions : null}
|
||||
{!desktopSidebarActionsInline && desktopRightSidebarActionsHost
|
||||
? createPortal(desktopSidebarActions, desktopRightSidebarActionsHost)
|
||||
|
||||
@@ -3,17 +3,20 @@ import { RiExternalLinkLine, RiGitBranchLine, RiPushpin2Fill, RiPushpin2Line } f
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChatContainer } from '@/components/chat/ChatContainer';
|
||||
import { ChatSurfaceProvider } from '@/components/chat/ChatSurfaceContext';
|
||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { invokeDesktop, isElectronShell } from '@/lib/desktop';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSessionWorktreeStore } from '@/sync/session-worktree-store';
|
||||
import { useSessions } from '@/sync/sync-context';
|
||||
import { useSessionMessages, useSessions } from '@/sync/sync-context';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useGitBranchLabel, useGitStore } from '@/stores/useGitStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||
|
||||
type MiniChatMode = 'session' | 'draft';
|
||||
|
||||
@@ -49,7 +52,10 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
const sessions = useSessions();
|
||||
const currentSessionMessages = useSessionMessages(currentSessionId ?? '');
|
||||
const runtimeApis = useRuntimeAPIs();
|
||||
const ensureGitStatus = useGitStore((state) => state.ensureStatus);
|
||||
const worktreePath = useSessionUIStore((state) => currentSessionId ? state.worktreeMetadata.get(currentSessionId)?.path ?? '' : '');
|
||||
@@ -129,9 +135,101 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
}, [session?.summary]);
|
||||
const changes = diffStats ?? { additions: 0, deletions: 0 };
|
||||
const hasChanges = changes.additions > 0 || changes.deletions > 0;
|
||||
const currentModel = getCurrentModel();
|
||||
const latestAssistantModel = React.useMemo(() => {
|
||||
for (let i = currentSessionMessages.length - 1; i >= 0; i -= 1) {
|
||||
const message = currentSessionMessages[i] as { role?: unknown; providerID?: unknown; modelID?: unknown };
|
||||
if (message.role !== 'assistant') continue;
|
||||
if (typeof message.providerID !== 'string' || typeof message.modelID !== 'string') continue;
|
||||
const provider = providers.find((entry) => entry.id === message.providerID);
|
||||
const model = provider?.models.find((entry) => entry.id === message.modelID);
|
||||
if (model) return model;
|
||||
}
|
||||
return undefined;
|
||||
}, [currentSessionMessages, providers]);
|
||||
const modelForLimits = currentModel?.limit ? currentModel : latestAssistantModel;
|
||||
const limit = modelForLimits && typeof modelForLimits.limit === 'object' && modelForLimits.limit !== null
|
||||
? (modelForLimits.limit as Record<string, unknown>)
|
||||
: null;
|
||||
const contextLimit = limit && typeof limit.context === 'number' ? limit.context : 0;
|
||||
const outputLimit = limit && typeof limit.output === 'number' ? limit.output : 0;
|
||||
const contextUsage = React.useMemo<SessionContextUsage | null>(() => {
|
||||
if (!currentSessionId || currentSessionMessages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
type AssistantTokens = { input: number; output: number; reasoning: number; cache: { read: number; write: number } };
|
||||
let lastTokens: AssistantTokens | undefined;
|
||||
let lastMessageId: string | undefined;
|
||||
|
||||
for (let i = currentSessionMessages.length - 1; i >= 0; i -= 1) {
|
||||
const message = currentSessionMessages[i];
|
||||
if (message.role !== 'assistant') continue;
|
||||
const tokens = (message as { tokens?: AssistantTokens }).tokens;
|
||||
if (!tokens) continue;
|
||||
const total = tokens.input + tokens.output + tokens.reasoning + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0);
|
||||
if (total > 0) {
|
||||
lastTokens = tokens;
|
||||
lastMessageId = message.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastTokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalTokens = lastTokens.input + lastTokens.output + lastTokens.reasoning + (lastTokens.cache?.read ?? 0) + (lastTokens.cache?.write ?? 0);
|
||||
const thresholdLimit = contextLimit > 0 ? contextLimit : 200000;
|
||||
const percentage = contextLimit > 0 ? Math.round((totalTokens / contextLimit) * 100) : 0;
|
||||
const normalizedOutput = outputLimit > 0 ? Math.round((lastTokens.output / outputLimit) * 100) : undefined;
|
||||
|
||||
return {
|
||||
totalTokens,
|
||||
percentage,
|
||||
contextLimit: contextLimit || 0,
|
||||
outputLimit: outputLimit || undefined,
|
||||
normalizedOutput,
|
||||
thresholdLimit,
|
||||
lastMessageId,
|
||||
};
|
||||
}, [contextLimit, currentSessionId, currentSessionMessages, outputLimit]);
|
||||
const [stableContextUsage, setStableContextUsage] = React.useState<SessionContextUsage | null>(null);
|
||||
const dragRegionStyle = { WebkitAppRegion: 'drag' } as React.CSSProperties;
|
||||
const noDragRegionStyle = { WebkitAppRegion: 'no-drag' } as React.CSSProperties;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentSessionId) {
|
||||
setStableContextUsage((prev) => (prev === null ? prev : null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextUsage && contextUsage.totalTokens > 0) {
|
||||
setStableContextUsage((prev) => {
|
||||
if (
|
||||
prev
|
||||
&& prev.totalTokens === contextUsage.totalTokens
|
||||
&& prev.percentage === contextUsage.percentage
|
||||
&& prev.contextLimit === contextUsage.contextLimit
|
||||
&& (prev.outputLimit ?? 0) === (contextUsage.outputLimit ?? 0)
|
||||
&& (prev.normalizedOutput ?? 0) === (contextUsage.normalizedOutput ?? 0)
|
||||
&& prev.thresholdLimit === contextUsage.thresholdLimit
|
||||
&& prev.lastMessageId === contextUsage.lastMessageId
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return contextUsage;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setStableContextUsage((prev) => (prev === null ? prev : null));
|
||||
}, [contextUsage, currentSessionId]);
|
||||
|
||||
const displayContextPercentage = stableContextUsage && stableContextUsage.contextLimit > 0
|
||||
? Math.min(999, (stableContextUsage.totalTokens / stableContextUsage.contextLimit) * 100)
|
||||
: 0;
|
||||
|
||||
const handleTogglePinned = React.useCallback(() => {
|
||||
const nextPinned = !pinned;
|
||||
setPinned(nextPinned);
|
||||
@@ -181,6 +279,20 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{stableContextUsage && stableContextUsage.totalTokens > 0 ? (
|
||||
<ContextUsageDisplay
|
||||
totalTokens={stableContextUsage.totalTokens}
|
||||
percentage={displayContextPercentage}
|
||||
colorPercentage={stableContextUsage.percentage}
|
||||
contextLimit={stableContextUsage.contextLimit}
|
||||
outputLimit={stableContextUsage.outputLimit ?? 0}
|
||||
className="h-9 shrink-0 pl-1 pr-1 typography-ui-label"
|
||||
valueClassName="font-semibold leading-none"
|
||||
hideIcon
|
||||
showPercentIcon
|
||||
percentIconClassName="h-5 w-5"
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
Reference in New Issue
Block a user