feat(ui): 优化 Header 和状态栏布局
- ContextUsage: 紧凑模式下始终显示压缩按钮 - StatusBar: 连接状态移至中间位置,显示绿点动画和文字 - StatusBar: 添加内部连接状态检测(通过 health API) - Chat: 移除 Header 中的连接状态指示器
This commit is contained in:
@@ -152,9 +152,27 @@ export function ContextUsage({
|
|||||||
</div>
|
</div>
|
||||||
{/* 数值 */}
|
{/* 数值 */}
|
||||||
<span className={cn('text-xs', textColor)}>{formatted}</span>
|
<span className={cn('text-xs', textColor)}>{formatted}</span>
|
||||||
{/* 警告图标 */}
|
{/* 压缩按钮 */}
|
||||||
{shouldCompress && (
|
{showCompressButton && (
|
||||||
<AlertTriangle size={12} className="text-amber-500" />
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-5 px-1.5 text-xs',
|
||||||
|
shouldCompress
|
||||||
|
? 'text-amber-500 hover:text-amber-400 hover:bg-amber-500/10'
|
||||||
|
: 'text-fg-muted hover:text-fg-secondary hover:bg-surface-muted'
|
||||||
|
)}
|
||||||
|
onClick={handleCompress}
|
||||||
|
disabled={compressing}
|
||||||
|
title="压缩对话上下文"
|
||||||
|
>
|
||||||
|
{compressing ? (
|
||||||
|
<RefreshCw size={12} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Zap size={12} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { GitBranch, AlertTriangle, AlertCircle, Wifi, WifiOff, RefreshCw, CheckCircle } from 'lucide-react';
|
import { GitBranch, AlertTriangle, AlertCircle, WifiOff, RefreshCw, CheckCircle } from 'lucide-react';
|
||||||
import { cn } from '../utils/cn.js';
|
import { cn } from '../utils/cn.js';
|
||||||
import { getLSPDiagnostics, getGitInfo, type DiagnosticsSummary, type GitInfo } from '../api/client.js';
|
import { getLSPDiagnostics, getGitInfo, getHealth, type DiagnosticsSummary, type GitInfo } from '../api/client.js';
|
||||||
|
|
||||||
interface StatusBarProps {
|
interface StatusBarProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -21,13 +21,17 @@ interface StatusBarProps {
|
|||||||
|
|
||||||
export function StatusBar({
|
export function StatusBar({
|
||||||
className,
|
className,
|
||||||
isConnected = true,
|
isConnected: isConnectedProp,
|
||||||
onDiagnosticsClick,
|
onDiagnosticsClick,
|
||||||
refreshInterval = 30000,
|
refreshInterval = 30000,
|
||||||
}: StatusBarProps) {
|
}: StatusBarProps) {
|
||||||
const [diagnostics, setDiagnostics] = useState<DiagnosticsSummary | null>(null);
|
const [diagnostics, setDiagnostics] = useState<DiagnosticsSummary | null>(null);
|
||||||
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null);
|
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState(true);
|
||||||
|
|
||||||
|
// 如果外部传入了 isConnected,使用外部值;否则使用内部检测
|
||||||
|
const isConnected = isConnectedProp ?? connectionStatus;
|
||||||
|
|
||||||
// 加载诊断信息和 Git 信息
|
// 加载诊断信息和 Git 信息
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
@@ -45,12 +49,37 @@ export function StatusBar({
|
|||||||
if (gitResult.success && gitResult.data) {
|
if (gitResult.success && gitResult.data) {
|
||||||
setGitInfo(gitResult.data);
|
setGitInfo(gitResult.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有外部传入连接状态,内部检测
|
||||||
|
if (isConnectedProp === undefined) {
|
||||||
|
setConnectionStatus(true);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略错误
|
// 请求失败说明可能断开连接
|
||||||
|
if (isConnectedProp === undefined) {
|
||||||
|
setConnectionStatus(false);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [isConnectedProp]);
|
||||||
|
|
||||||
|
// 检测连接状态(如果没有外部传入)
|
||||||
|
const checkConnection = useCallback(async () => {
|
||||||
|
if (isConnectedProp !== undefined) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getHealth();
|
||||||
|
setConnectionStatus(true);
|
||||||
|
} catch {
|
||||||
|
setConnectionStatus(false);
|
||||||
|
}
|
||||||
|
}, [isConnectedProp]);
|
||||||
|
|
||||||
|
// 初始连接检测
|
||||||
|
useEffect(() => {
|
||||||
|
checkConnection();
|
||||||
|
}, [checkConnection]);
|
||||||
|
|
||||||
// 初始加载和定时刷新
|
// 初始加载和定时刷新
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -94,6 +123,30 @@ export function StatusBar({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 中间 - 连接状态 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5',
|
||||||
|
isConnected ? 'text-green-400' : 'text-red-400'
|
||||||
|
)}
|
||||||
|
title={isConnected ? 'Connected to server' : 'Disconnected from server'}
|
||||||
|
>
|
||||||
|
{isConnected ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
<span>Connected</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WifiOff size={12} />
|
||||||
|
<span>Disconnected</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 右侧 */}
|
{/* 右侧 */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* 诊断信息 */}
|
{/* 诊断信息 */}
|
||||||
@@ -123,17 +176,6 @@ export function StatusBar({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 连接状态 */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-1',
|
|
||||||
isConnected ? 'text-green-400' : 'text-red-400'
|
|
||||||
)}
|
|
||||||
title={isConnected ? 'Connected to server' : 'Disconnected from server'}
|
|
||||||
>
|
|
||||||
{isConnected ? <Wifi size={12} /> : <WifiOff size={12} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { WifiOff, MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react';
|
import { MessageSquare, Terminal, Plug, Zap, Bot, History, Server, MessagesSquare } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@@ -129,30 +129,6 @@ export function ChatPage({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 连接状态指示器
|
|
||||||
const ConnectionStatus = () => (
|
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
|
||||||
{isConnected ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span className="relative flex h-2.5 w-2.5">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
|
|
||||||
</span>
|
|
||||||
<span className="text-green-400 hidden sm:inline">Connected</span>
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1.5 text-red-400">
|
|
||||||
<WifiOff size={16} />
|
|
||||||
<span className="hidden sm:inline">Disconnected</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col h-full">
|
<div className="flex-1 flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -164,14 +140,11 @@ export function ChatPage({
|
|||||||
<ContextUsage
|
<ContextUsage
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
compact
|
compact
|
||||||
showCompressButton={false}
|
showCompressButton
|
||||||
refreshInterval={30000}
|
refreshInterval={30000}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 连接状态 */}
|
|
||||||
<ConnectionStatus />
|
|
||||||
|
|
||||||
{/* 工具栏按钮 */}
|
{/* 工具栏按钮 */}
|
||||||
{(onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics || onOpenSessions) && (
|
{(onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents || onOpenCheckpoints || onOpenProviders || onOpenLSP || onOpenDiagnostics || onOpenSessions) && (
|
||||||
<div className="flex items-center gap-1.5 border-l border-line-muted pl-3">
|
<div className="flex items-center gap-1.5 border-l border-line-muted pl-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user