diff --git a/packages/desktop/src/App.tsx b/packages/desktop/src/App.tsx
index 66fb255..332a5b1 100644
--- a/packages/desktop/src/App.tsx
+++ b/packages/desktop/src/App.tsx
@@ -2,7 +2,7 @@
* App Component
*/
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import {
Sidebar,
FileBrowser,
@@ -19,6 +19,7 @@ export function App() {
const [isInitializing, setIsInitializing] = useState(true);
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showConfig, setShowConfig] = useState(false);
+ const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
// 初始化:加载或创建会话
useEffect(() => {
@@ -52,6 +53,11 @@ export function App() {
setCurrentSessionId(session.id);
};
+ // 会话标题更新回调
+ const handleSessionUpdated = useCallback((sessionId: string, name: string) => {
+ setSessionTitleUpdate({ sessionId, name });
+ }, []);
+
if (isInitializing) {
return (
@@ -69,6 +75,7 @@ export function App() {
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onCreateSession={handleCreateSession}
+ sessionTitleUpdate={sessionTitleUpdate}
/>
@@ -78,6 +85,7 @@ export function App() {
setShowFileBrowser(!showFileBrowser)}
onOpenConfig={() => setShowConfig(true)}
diff --git a/packages/desktop/src/pages/Chat.tsx b/packages/desktop/src/pages/Chat.tsx
index 62a5993..16f7518 100644
--- a/packages/desktop/src/pages/Chat.tsx
+++ b/packages/desktop/src/pages/Chat.tsx
@@ -15,6 +15,7 @@ import {
interface ChatPageProps {
sessionId: string;
+ onSessionUpdated?: (sessionId: string, name: string) => void;
// 工具栏按钮
showFileBrowser?: boolean;
onToggleFileBrowser?: () => void;
@@ -23,6 +24,7 @@ interface ChatPageProps {
export function ChatPage({
sessionId,
+ onSessionUpdated,
showFileBrowser,
onToggleFileBrowser,
onOpenConfig,
@@ -39,6 +41,7 @@ export function ChatPage({
onError: (error) => {
console.error('Chat error:', error);
},
+ onSessionUpdated,
});
const messagesEndRef = useRef(null);
diff --git a/packages/server/src/agent/adapter.ts b/packages/server/src/agent/adapter.ts
index f5635bf..e681539 100644
--- a/packages/server/src/agent/adapter.ts
+++ b/packages/server/src/agent/adapter.ts
@@ -196,6 +196,20 @@ export async function processMessage(sessionId: string, content: string): Promis
payload: assistantMessage,
});
+ // 检查是否需要生成会话标题(首次对话完成后)
+ const session = sessionManager.get(sessionId);
+ const messages = sessionManager.getMessages(sessionId);
+ if (session && !session.name && messages.length === 2) {
+ // 首条用户消息 + 首条 AI 回复 = 2 条消息
+ const userMessage = messages.find(m => m.role === 'user');
+ if (userMessage) {
+ // 异步生成标题,不阻塞响应
+ generateSessionTitle(sessionId, userMessage.content, response).catch(err => {
+ console.error('[Agent] Failed to generate session title:', err);
+ });
+ }
+ }
+
emitStatusEvent(sessionId, 'idle');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -247,3 +261,62 @@ export function getAgentStats(sessionId: string): {
contextUsage: agent.getContextUsageFormatted(),
};
}
+
+// ============================================================================
+// 会话标题生成
+// ============================================================================
+
+/**
+ * 从用户消息中提取简短标题
+ * 使用简单的启发式方法,不依赖 LLM
+ */
+function extractTitleFromMessage(userMessage: string): string {
+ // 移除多余空白
+ const cleaned = userMessage.trim().replace(/\s+/g, ' ');
+
+ // 如果消息很短,直接使用
+ if (cleaned.length <= 30) {
+ return cleaned;
+ }
+
+ // 尝试提取第一句话
+ const firstSentence = cleaned.match(/^[^。!?.!?]+[。!?.!?]?/)?.[0] || cleaned;
+
+ // 如果第一句话也很长,截断
+ if (firstSentence.length > 40) {
+ return firstSentence.slice(0, 37) + '...';
+ }
+
+ return firstSentence;
+}
+
+/**
+ * 生成会话标题并更新
+ */
+async function generateSessionTitle(
+ sessionId: string,
+ userMessage: string,
+ _assistantResponse: string
+): Promise {
+ const sessionManager = getSessionManager();
+
+ // 使用简单提取方式生成标题
+ const title = extractTitleFromMessage(userMessage);
+
+ // 更新会话标题
+ const updatedSession = await sessionManager.updateSessionName(sessionId, title);
+
+ if (updatedSession) {
+ // 广播标题更新事件
+ broadcastToSession(sessionId, {
+ type: 'session_updated',
+ sessionId,
+ payload: {
+ id: updatedSession.id,
+ name: updatedSession.name,
+ },
+ });
+
+ console.log(`[Agent] Session title generated: "${title}"`);
+ }
+}
diff --git a/packages/server/src/session/manager.ts b/packages/server/src/session/manager.ts
index e357bc6..cf9c5a2 100644
--- a/packages/server/src/session/manager.ts
+++ b/packages/server/src/session/manager.ts
@@ -253,6 +253,22 @@ export class SessionManager {
return fullMessage;
}
+ /**
+ * 更新会话名称/标题
+ */
+ async updateSessionName(sessionId: string, name: string): Promise {
+ const session = this.sessions.get(sessionId);
+ if (!session) return undefined;
+
+ session.name = name;
+ session.updatedAt = new Date().toISOString();
+
+ // 持久化
+ await this.persist(sessionId);
+
+ return session;
+ }
+
/**
* 获取会话数量
*/
diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts
index bc7c32e..00aa5bc 100644
--- a/packages/server/src/types.ts
+++ b/packages/server/src/types.ts
@@ -108,7 +108,8 @@ export interface ServerMessage {
| 'tool_result'
| 'done'
| 'cancelled'
- | 'error';
+ | 'error'
+ | 'session_updated';
sessionId: string;
payload?: unknown;
}
diff --git a/packages/ui/src/components/Sidebar.tsx b/packages/ui/src/components/Sidebar.tsx
index 21580bf..b4708e5 100644
--- a/packages/ui/src/components/Sidebar.tsx
+++ b/packages/ui/src/components/Sidebar.tsx
@@ -19,6 +19,8 @@ interface SidebarProps {
onCreateSession: (session: Session) => void;
/** 是否启用响应式布局(移动端抽屉式菜单) */
responsive?: boolean;
+ /** 会话标题更新事件(从 WebSocket 接收) */
+ sessionTitleUpdate?: { sessionId: string; name: string } | null;
}
export function Sidebar({
@@ -26,6 +28,7 @@ export function Sidebar({
onSelectSession,
onCreateSession,
responsive = false,
+ sessionTitleUpdate,
}: SidebarProps) {
const [sessions, setSessions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
@@ -84,6 +87,19 @@ export function Sidebar({
loadSessions();
}, []);
+ // 处理会话标题更新
+ useEffect(() => {
+ if (sessionTitleUpdate) {
+ setSessions((prev) =>
+ prev.map((s) =>
+ s.id === sessionTitleUpdate.sessionId
+ ? { ...s, name: sessionTitleUpdate.name }
+ : s
+ )
+ );
+ }
+ }, [sessionTitleUpdate]);
+
const handleOverlayClick = () => {
setIsOpen(false);
};
diff --git a/packages/ui/src/hooks/useChat.ts b/packages/ui/src/hooks/useChat.ts
index 865d45c..19a3944 100644
--- a/packages/ui/src/hooks/useChat.ts
+++ b/packages/ui/src/hooks/useChat.ts
@@ -11,6 +11,7 @@ interface UseChatOptions {
sessionId: string;
onError?: (error: Error) => void;
onSessionNotFound?: () => void;
+ onSessionUpdated?: (sessionId: string, name: string) => void;
}
interface ChatState {
@@ -20,7 +21,7 @@ interface ChatState {
streamingContent: string;
}
-export function useChat({ sessionId, onError, onSessionNotFound }: UseChatOptions) {
+export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated }: UseChatOptions) {
const [state, setState] = useState({
messages: [],
isConnected: false,
@@ -38,8 +39,10 @@ export function useChat({ sessionId, onError, onSessionNotFound }: UseChatOption
// 用 ref 存储回调,避免依赖变化导致无限循环
const onErrorRef = useRef(onError);
const onSessionNotFoundRef = useRef(onSessionNotFound);
+ const onSessionUpdatedRef = useRef(onSessionUpdated);
onErrorRef.current = onError;
onSessionNotFoundRef.current = onSessionNotFound;
+ onSessionUpdatedRef.current = onSessionUpdated;
// 加载历史消息
const loadMessages = useCallback(async () => {
@@ -134,6 +137,13 @@ export function useChat({ sessionId, onError, onSessionNotFound }: UseChatOption
onErrorRef.current?.(new Error(message.payload?.message || 'Unknown error'));
setState((prev) => ({ ...prev, isLoading: false, streamingContent: '' }));
break;
+
+ case 'session_updated':
+ // 会话信息更新(如标题)
+ if (message.payload?.id && message.payload?.name) {
+ onSessionUpdatedRef.current?.(message.payload.id, message.payload.name);
+ }
+ break;
}
} catch {
// 忽略解析错误
diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx
index 1b26e0e..fd42c91 100644
--- a/packages/web/src/App.tsx
+++ b/packages/web/src/App.tsx
@@ -21,6 +21,7 @@ export function App() {
const [isInitializing, setIsInitializing] = useState(true);
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showConfig, setShowConfig] = useState(false);
+ const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
// 初始化:加载或创建会话
useEffect(() => {
@@ -64,6 +65,11 @@ export function App() {
}
}, []);
+ // 会话标题更新回调
+ const handleSessionUpdated = useCallback((sessionId: string, name: string) => {
+ setSessionTitleUpdate({ sessionId, name });
+ }, []);
+
if (isInitializing) {
return (
@@ -82,6 +88,7 @@ export function App() {
onSelectSession={handleSelectSession}
onCreateSession={handleCreateSession}
responsive
+ sessionTitleUpdate={sessionTitleUpdate}
/>
{/* 主内容区域 */}
@@ -93,6 +100,7 @@ export function App() {
key={currentSessionId}
sessionId={currentSessionId}
onSessionNotFound={handleSessionNotFound}
+ onSessionUpdated={handleSessionUpdated}
responsive
showFileBrowser={showFileBrowser}
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
diff --git a/packages/web/src/pages/Chat.tsx b/packages/web/src/pages/Chat.tsx
index b79fd8a..aaad472 100644
--- a/packages/web/src/pages/Chat.tsx
+++ b/packages/web/src/pages/Chat.tsx
@@ -16,6 +16,7 @@ import {
interface ChatPageProps {
sessionId: string;
onSessionNotFound?: () => void;
+ onSessionUpdated?: (sessionId: string, name: string) => void;
responsive?: boolean;
// 工具栏按钮
showFileBrowser?: boolean;
@@ -26,6 +27,7 @@ interface ChatPageProps {
export function ChatPage({
sessionId,
onSessionNotFound,
+ onSessionUpdated,
responsive = false,
showFileBrowser,
onToggleFileBrowser,
@@ -44,6 +46,7 @@ export function ChatPage({
console.error('Chat error:', error);
},
onSessionNotFound,
+ onSessionUpdated,
});
const messagesEndRef = useRef(null);