diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx
index 0d8f55f..152d06a 100644
--- a/desktop/src/components/ChatArea.tsx
+++ b/desktop/src/components/ChatArea.tsx
@@ -14,7 +14,8 @@ import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Im
import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
import { ResizableChatLayout } from './ai/ResizableChatLayout';
import { ArtifactPanel } from './ai/ArtifactPanel';
-import { ToolCallChain } from './ai/ToolCallChain';
+import { ToolCallChain, type ToolCallStep } from './ai/ToolCallChain';
+import { TaskProgress, type Subtask } from './ai/TaskProgress';
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
import { FirstConversationPrompt } from './FirstConversationPrompt';
import { ClassroomPlayer } from './classroom_player';
@@ -30,7 +31,6 @@ import { ReasoningBlock } from './ai/ReasoningBlock';
import { StreamingText } from './ai/StreamingText';
import { ChatMode } from './ai/ChatMode';
import { ModelSelector } from './ai/ModelSelector';
-import { TaskProgress } from './ai/TaskProgress';
import { SuggestionChips } from './ai/SuggestionChips';
import { PipelineResultPreview } from './pipeline/PipelineResultPreview';
import { PresentationContainer } from './presentation/PresentationContainer';
@@ -598,6 +598,20 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
const isUser = message.role === 'user';
const isThinking = message.streaming && !message.content;
+ // Extract typed arrays for JSX rendering (avoids TS2322 from && chain producing unknown)
+ const toolCallSteps: ToolCallStep[] | undefined = message.toolSteps;
+ const subtaskList: Subtask[] | undefined = message.subtasks;
+
+ const renderToolSteps = (): React.ReactNode => {
+ if (isUser || !toolCallSteps || toolCallSteps.length === 0) return null;
+ return ;
+ };
+
+ const renderSubtasks = (): React.ReactNode => {
+ if (isUser || !subtaskList || subtaskList.length === 0) return null;
+ return ;
+ };
+
// Download message as Markdown file
const handleDownloadMessage = () => {
if (!message.content) return;
@@ -644,16 +658,9 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
/>
)}
{/* Tool call steps chain (DeerFlow-inspired) */}
- {!isUser && message.toolSteps != null && message.toolSteps.length > 0 ? (
-
- ) : null}
+ {renderToolSteps()}
{/* Subtask tracking (DeerFlow-inspired) */}
- {!isUser && message.subtasks != null && message.subtasks.length > 0 ? (
-
- ) : null}
+ {renderSubtasks()}
{/* Message content with streaming support */}
{message.content
diff --git a/desktop/src/components/SaaS/SubscriptionPanel.tsx b/desktop/src/components/SaaS/SubscriptionPanel.tsx
new file mode 100644
index 0000000..2169aa0
--- /dev/null
+++ b/desktop/src/components/SaaS/SubscriptionPanel.tsx
@@ -0,0 +1,173 @@
+/**
+ * SubscriptionPanel — 当前订阅详情面板
+ *
+ * 展示当前计划、试用/到期状态、用量配额和操作入口。
+ */
+
+import { useEffect } from 'react';
+import { useSaaSStore } from '../../store/saasStore';
+import {
+ CheckCircle,
+ Clock,
+ AlertTriangle,
+ Loader2,
+ CreditCard,
+ Crown,
+} from 'lucide-react';
+
+const PLAN_LABELS: Record
= {
+ free: '免费版',
+ pro: '专业版',
+ team: '团队版',
+};
+
+const PLAN_COLORS: Record = {
+ free: '#8c8c8c',
+ pro: '#863bff',
+ team: '#47bfff',
+};
+
+function UsageBar({ label, current, max }: { label: string; current: number; max: number | null }) {
+ const pct = max ? Math.min((current / max) * 100, 100) : 0;
+ const displayMax = max ? max.toLocaleString() : '∞';
+ const barColor = pct >= 90 ? 'bg-red-500' : pct >= 70 ? 'bg-amber-500' : 'bg-emerald-500';
+
+ return (
+
+
+ {label}
+ {current.toLocaleString()} / {displayMax}
+
+
+
+ );
+}
+
+export function SubscriptionPanel() {
+ const subscription = useSaaSStore((s) => s.subscription);
+ const billingLoading = useSaaSStore((s) => s.billingLoading);
+ const fetchBillingOverview = useSaaSStore((s) => s.fetchBillingOverview);
+
+ useEffect(() => {
+ fetchBillingOverview().catch(() => {});
+ }, [fetchBillingOverview]);
+
+ if (billingLoading && !subscription) {
+ return (
+
+
+
+ );
+ }
+
+ const plan = subscription?.plan;
+ const sub = subscription?.subscription;
+ const usage = subscription?.usage;
+ const planName = plan?.name || 'free';
+ const color = PLAN_COLORS[planName] || '#666';
+ const label = PLAN_LABELS[planName] || planName;
+
+ // Trial / expiry status
+ const isTrialing = sub?.status === 'trialing';
+ const isActive = sub?.status === 'active';
+ const trialEnd = sub?.trial_end ? new Date(sub.trial_end) : null;
+ const periodEnd = sub?.current_period_end ? new Date(sub.current_period_end) : null;
+
+ const now = Date.now();
+ const daysLeft = trialEnd
+ ? Math.max(0, Math.ceil((trialEnd.getTime() - now) / (1000 * 60 * 60 * 24)))
+ : periodEnd
+ ? Math.max(0, Math.ceil((periodEnd.getTime() - now) / (1000 * 60 * 60 * 24)))
+ : null;
+
+ const showExpiryWarning = daysLeft !== null && daysLeft <= 3;
+
+ return (
+
+ {/* Plan badge */}
+
+
+
+
+
+
+
+
{label}
+ {isTrialing && (
+
+
+ 试用中
+
+ )}
+ {isActive && (
+
+
+ 已激活
+
+ )}
+
+
+
+
+
+ {/* Expiry / trial warning */}
+ {showExpiryWarning && (
+
+
+
+ {isTrialing
+ ? `试用还剩 ${daysLeft} 天,请尽快升级以保留 Pro 功能`
+ : `订阅将在 ${daysLeft} 天后到期`}
+
+
+ )}
+
+ {daysLeft !== null && !showExpiryWarning && (
+
+ {isTrialing ? `试用剩余 ${daysLeft} 天` : `订阅到期: ${periodEnd?.toLocaleDateString()}`}
+
+ )}
+
+
+ {/* Usage */}
+ {usage && (
+
+
用量配额
+
+
+
+ {sub && (
+
+ 周期: {new Date(sub.current_period_start).toLocaleDateString()} — {new Date(sub.current_period_end).toLocaleDateString()}
+
+ )}
+
+ )}
+
+ {/* Free plan upgrade prompt */}
+ {planName === 'free' && (
+
+
升级到专业版
+
+ 解锁全部 9 个 Hands、无限 Pipeline 和完整记忆系统
+
+
+ 请前往「订阅与计费」页面选择计划并完成支付
+
+
+ )}
+
+ );
+}