refactor IPC (#341)

This commit is contained in:
Lingxuan Zuo
2026-03-08 11:54:49 +08:00
committed by GitHub
Unverified
parent c03d92e9a2
commit 3d804a9f5e
52 changed files with 3121 additions and 336 deletions

View File

@@ -3,7 +3,7 @@
* Navigation sidebar with menu items.
* No longer fixed - sits inside the flex layout below the title bar.
*/
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import {
Home,
@@ -24,8 +24,17 @@ import { useChatStore } from '@/stores/chat';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { invokeIpc } from '@/lib/api-client';
import { useTranslation } from 'react-i18next';
type SessionBucketKey =
| 'today'
| 'yesterday'
| 'withinWeek'
| 'withinTwoWeeks'
| 'withinMonth'
| 'older';
interface NavItemProps {
to: string;
icon: React.ReactNode;
@@ -66,6 +75,25 @@ function NavItem({ to, icon, label, badge, collapsed, onClick }: NavItemProps) {
);
}
function getSessionBucket(activityMs: number, nowMs: number): SessionBucketKey {
if (!activityMs || activityMs <= 0) return 'older';
const now = new Date(nowMs);
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
if (activityMs >= startOfToday) return 'today';
if (activityMs >= startOfYesterday) return 'yesterday';
const daysAgo = (startOfToday - activityMs) / (24 * 60 * 60 * 1000);
if (daysAgo <= 7) return 'withinWeek';
if (daysAgo <= 14) return 'withinTwoWeeks';
if (daysAgo <= 30) return 'withinMonth';
return 'older';
}
const INITIAL_NOW_MS = Date.now();
export function Sidebar() {
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
@@ -82,15 +110,12 @@ export function Sidebar() {
const navigate = useNavigate();
const isOnChat = useLocation().pathname === '/';
const mainSessions = sessions.filter((s) => s.key.endsWith(':main'));
const otherSessions = sessions.filter((s) => !s.key.endsWith(':main'));
const getSessionLabel = (key: string, displayName?: string, label?: string) =>
sessionLabels[key] ?? label ?? displayName ?? key;
const openDevConsole = async () => {
try {
const result = await window.electron.ipcRenderer.invoke('gateway:getControlUiUrl') as {
const result = await invokeIpc('gateway:getControlUiUrl') as {
success: boolean;
url?: string;
error?: string;
@@ -105,8 +130,35 @@ export function Sidebar() {
}
};
const { t } = useTranslation();
const { t } = useTranslation(['common', 'chat']);
const [sessionToDelete, setSessionToDelete] = useState<{ key: string; label: string } | null>(null);
const [nowMs, setNowMs] = useState(INITIAL_NOW_MS);
useEffect(() => {
const timer = window.setInterval(() => {
setNowMs(Date.now());
}, 60 * 1000);
return () => window.clearInterval(timer);
}, []);
const sessionBuckets: Array<{ key: SessionBucketKey; label: string; sessions: typeof sessions }> = [
{ key: 'today', label: t('chat:historyBuckets.today'), sessions: [] },
{ key: 'yesterday', label: t('chat:historyBuckets.yesterday'), sessions: [] },
{ key: 'withinWeek', label: t('chat:historyBuckets.withinWeek'), sessions: [] },
{ key: 'withinTwoWeeks', label: t('chat:historyBuckets.withinTwoWeeks'), sessions: [] },
{ key: 'withinMonth', label: t('chat:historyBuckets.withinMonth'), sessions: [] },
{ key: 'older', label: t('chat:historyBuckets.older'), sessions: [] },
];
const sessionBucketMap = Object.fromEntries(sessionBuckets.map((bucket) => [bucket.key, bucket])) as Record<
SessionBucketKey,
(typeof sessionBuckets)[number]
>;
for (const session of [...sessions].sort((a, b) =>
(sessionLastActivity[b.key] ?? 0) - (sessionLastActivity[a.key] ?? 0)
)) {
const bucketKey = getSessionBucket(sessionLastActivity[session.key] ?? 0, nowMs);
sessionBucketMap[bucketKey].sessions.push(session);
}
const navItems = [
{ to: '/cron', icon: <Clock className="h-5 w-5" />, label: t('sidebar.cronTasks') },
@@ -153,43 +205,47 @@ export function Sidebar() {
{/* Session list — below Settings, only when expanded */}
{!sidebarCollapsed && sessions.length > 0 && (
<div className="mt-1 overflow-y-auto max-h-72 space-y-0.5">
{[...mainSessions, ...[...otherSessions].sort((a, b) =>
(sessionLastActivity[b.key] ?? 0) - (sessionLastActivity[a.key] ?? 0)
)].map((s) => (
<div key={s.key} className="group relative flex items-center">
<button
onClick={() => { switchSession(s.key); navigate('/'); }}
className={cn(
'w-full text-left rounded-md px-3 py-1.5 text-sm truncate transition-colors',
!s.key.endsWith(':main') && 'pr-7',
'hover:bg-accent hover:text-accent-foreground',
isOnChat && currentSessionKey === s.key
? 'bg-accent/60 text-accent-foreground font-medium'
: 'text-muted-foreground',
)}
>
{getSessionLabel(s.key, s.displayName, s.label)}
</button>
{!s.key.endsWith(':main') && (
<button
aria-label="Delete session"
onClick={(e) => {
e.stopPropagation();
setSessionToDelete({
key: s.key,
label: getSessionLabel(s.key, s.displayName, s.label),
});
}}
className={cn(
'absolute right-1 flex items-center justify-center rounded p-0.5 transition-opacity',
'opacity-0 group-hover:opacity-100',
'text-muted-foreground hover:text-destructive hover:bg-destructive/10',
)}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
)}
</div>
{sessionBuckets.map((bucket) => (
bucket.sessions.length > 0 ? (
<div key={bucket.key} className="pt-1">
<div className="px-3 py-1 text-[11px] font-medium text-muted-foreground/80">
{bucket.label}
</div>
{bucket.sessions.map((s) => (
<div key={s.key} className="group relative flex items-center">
<button
onClick={() => { switchSession(s.key); navigate('/'); }}
className={cn(
'w-full text-left rounded-md px-3 py-1.5 text-sm truncate transition-colors pr-7',
'hover:bg-accent hover:text-accent-foreground',
isOnChat && currentSessionKey === s.key
? 'bg-accent/60 text-accent-foreground font-medium'
: 'text-muted-foreground',
)}
>
{getSessionLabel(s.key, s.displayName, s.label)}
</button>
<button
aria-label="Delete session"
onClick={(e) => {
e.stopPropagation();
setSessionToDelete({
key: s.key,
label: getSessionLabel(s.key, s.displayName, s.label),
});
}}
className={cn(
'absolute right-1 flex items-center justify-center rounded p-0.5 transition-opacity',
'opacity-0 group-hover:opacity-100',
'text-muted-foreground hover:text-destructive hover:bg-destructive/10',
)}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
) : null
))}
</div>
)}