diff --git a/web/src/App.tsx b/web/src/App.tsx
index 75ed1a64..35d564aa 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,8 +1,7 @@
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import { AppShell } from './components/layout/AppShell';
import { DocumentPane } from './components/doc/DocumentPane';
-import { MessageStream } from './components/chat/MessageStream';
-import { InputArea } from './components/chat/InputArea';
+import { ChatColumn } from './components/chat/ChatColumn';
import { fetchFreeformDoc } from './api/freeformDoc';
import type { DiffResult, OverlayMode } from './features/documentRevision/types';
import { fetchProjectLineage, getProject } from './api/projectWorkflow';
@@ -35,7 +34,6 @@ import {
import { DocumentLineage } from './types/projectWorkflow';
import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice';
import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner';
-import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard';
import { UserProjectRightsModal } from './components/user/UserProjectRightsModal';
import { UserSettingsModal } from './components/user/UserSettingsModal';
import { OnDemand } from './components/lazy/OnDemand';
@@ -114,13 +112,8 @@ import { RequestEpoch, shareInflight } from './data/initFanout';
import {
resolveSessionProjectId,
} from './lib/sessionAdapter';
-import { CategoryFilterBar } from './components/history/CategoryFilterBar';
-import { HistoryCalendar } from './components/history/HistoryCalendar';
-import { ChatSearchModal } from './components/history/ChatSearchModal';
-import { DigestTimelineModal } from './components/history/DigestTimelineModal';
-import { navigateToAnchor, parseAnchorHash, resolveTurnAnchorId } from './engine/anchorNavigator';
+import { navigateToAnchor, parseAnchorHash } from './engine/anchorNavigator';
import { useUnreadMarks } from './hooks/useUnreadMarks';
-import { CategoryDock } from './components/chat/CategoryDock';
import { fetchChatCategories, fetchChatDates } from './api/chatWindow';
import { ChatCategoryStat, ChatDateStat, QuotedMessage } from './types/workbench';
import { saveFile } from './data/fileStore';
@@ -134,9 +127,6 @@ export {
const loadAdminConsoleView = () =>
import('./components/admin/console/AdminConsoleView').then((m) => ({ default: m.AdminConsoleView }));
-const loadChatExportModal = () =>
- import('./components/history/ChatExportModal').then((m) => ({ default: m.ChatExportModal }));
-
export default function App() {
// Wave 2:启动时幂等剔除 localStorage 中的 demo seed 残留(须先于 load*)
purgeDemoSeedsFromLocalStorage();
@@ -1553,156 +1543,42 @@ export default function App() {
}}
workflowBanner={workflowBanner}
chatContent={
-
- {/* P1, P2, P3: 顶部分类统计过滤栏、历史日历、三模搜索、智能摘要、导出与未读角标 */}
-
{
- if (lastMessageId && activeSessionId) {
- const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
- navigateToAnchor(
- { sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
- { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
- );
- }
- }}
- onToggleCalendar={() => setIsCalendarOpen((prev) => !prev)}
- isCalendarOpen={isCalendarOpen}
- onOpenSearch={() => setIsSearchOpen(true)}
- onOpenDigest={() => setIsDigestOpen(true)}
- onOpenExport={() => setIsExportOpen(true)}
- unreadTotal={unreadTotal}
- />
- {isCalendarOpen && (
- {
- chatWindow.jumpToDate(date);
- setIsCalendarOpen(false);
- }}
- onClose={() => setIsCalendarOpen(false)}
- />
- )}
- {isSearchOpen && (
- {
- navigateToAnchor(
- { sessionId: activeSessionId, messageId: hit.message_id },
- { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
- );
- setIsSearchOpen(false);
- }}
- onClose={() => setIsSearchOpen(false)}
- />
- )}
- {isDigestOpen && (
- {
- navigateToAnchor(
- { sessionId: activeSessionId, messageId },
- { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
- );
- setIsDigestOpen(false);
- }}
- onClose={() => setIsDigestOpen(false)}
- />
- )}
- {isExportOpen && (
- setIsExportOpen(false),
- sessionId: activeSessionId,
- dayStack: chatWindow.displayDayStack,
- }}
- />
- )}
-
- {
- navigateToAnchor(
- { sessionId: activeSessionId, messageId: targetAnchor },
- { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
- );
- }}
- selectedCategory={chatWindow.selectedCategory}
- unreadMessageIds={unreadMessageIds}
- onSaveToPrivate={handleSaveMessageToPrivate}
- onExportDocx={handleExportReplyDocx}
- onOpenDoc={handleSelectDoc}
- />
- {
- const targetId = await consumeNextUnread();
- if (targetId && activeSessionId) {
- navigateToAnchor(
- { sessionId: activeSessionId, messageId: targetId, highlightMs: 2000 },
- { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
- );
- }
- }}
- onNavigateToCategory={(_cat, lastMessageId) => {
- if (lastMessageId && activeSessionId) {
- const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
- navigateToAnchor(
- { sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
- { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
- );
- }
- }}
- onSelectCategory={chatWindow.setSelectedCategory}
- />
-
- {pendingProposal && (
-
- {proposalError && (
-
- {proposalError}
-
- )}
-
-
- )}
-
-
+
}
docContent={
{
+ const app = readFileSync(new URL('../../App.tsx', import.meta.url), 'utf8');
+ const column = readFileSync(new URL('./ChatColumn.tsx', import.meta.url), 'utf8');
+ assert.equal((app.match(/
+ import('../history/ChatExportModal').then((m) => ({ default: m.ChatExportModal }));
+
+export interface ChatColumnProps {
+ activeSessionId: string;
+ categoryStats: ChatCategoryStat[];
+ chatWindow: UseChatWindowReturn;
+ isCalendarOpen: boolean;
+ setIsCalendarOpen: (next: boolean | ((prev: boolean) => boolean)) => void;
+ isSearchOpen: boolean;
+ setIsSearchOpen: (open: boolean) => void;
+ isDigestOpen: boolean;
+ setIsDigestOpen: (open: boolean) => void;
+ isExportOpen: boolean;
+ setIsExportOpen: (open: boolean) => void;
+ unreadTotal: number;
+ dateStats: ChatDateStat[];
+ unreadMarks: UnreadMark[];
+ unreadMessageIds: number[];
+ isAllReadCompleted: boolean;
+ consumeNextUnread: () => Promise;
+ handleSelectDoc: (id: string) => void;
+ handleCardAction: (cardId: string, actionId: string, payload?: unknown) => void;
+ handleErrorAction: (messageId: string, action: 'retry' | 'continue' | 'restart') => void;
+ handleQuoteMessage: (message: ChatMessage) => void;
+ handleSaveMessageToPrivate: (message: ChatMessage) => void;
+ handleExportReplyDocx: (message: ChatMessage) => void | Promise;
+ pendingProposal: { changes: string; impact: string; risk: string } | null;
+ proposalError: string | null;
+ handleAcceptProposal: () => void;
+ handleRejectProposal: () => void;
+ handleSendMessage: React.ComponentProps['onSendMessage'];
+ isThinking: boolean;
+ isStreamingActive: boolean;
+ handleStopStreaming: () => void;
+ activeQuotes: QuotedMessage[];
+ handleRemoveQuote: (index: number) => void;
+ isCurrentProjectExited: boolean;
+}
+
+export function ChatColumn({
+ activeSessionId,
+ categoryStats,
+ chatWindow,
+ isCalendarOpen,
+ setIsCalendarOpen,
+ isSearchOpen,
+ setIsSearchOpen,
+ isDigestOpen,
+ setIsDigestOpen,
+ isExportOpen,
+ setIsExportOpen,
+ unreadTotal,
+ dateStats,
+ unreadMarks,
+ unreadMessageIds,
+ isAllReadCompleted,
+ consumeNextUnread,
+ handleSelectDoc,
+ handleCardAction,
+ handleErrorAction,
+ handleQuoteMessage,
+ handleSaveMessageToPrivate,
+ handleExportReplyDocx,
+ pendingProposal,
+ proposalError,
+ handleAcceptProposal,
+ handleRejectProposal,
+ handleSendMessage,
+ isThinking,
+ isStreamingActive,
+ handleStopStreaming,
+ activeQuotes,
+ handleRemoveQuote,
+ isCurrentProjectExited,
+}: ChatColumnProps) {
+ return (
+
+
{
+ if (lastMessageId && activeSessionId) {
+ const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
+ navigateToAnchor(
+ { sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
+ { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
+ );
+ }
+ }}
+ onToggleCalendar={() => setIsCalendarOpen((prev) => !prev)}
+ isCalendarOpen={isCalendarOpen}
+ onOpenSearch={() => setIsSearchOpen(true)}
+ onOpenDigest={() => setIsDigestOpen(true)}
+ onOpenExport={() => setIsExportOpen(true)}
+ unreadTotal={unreadTotal}
+ />
+ {isCalendarOpen && (
+ {
+ chatWindow.jumpToDate(date);
+ setIsCalendarOpen(false);
+ }}
+ onClose={() => setIsCalendarOpen(false)}
+ />
+ )}
+ {isSearchOpen && (
+ {
+ navigateToAnchor(
+ { sessionId: activeSessionId, messageId: hit.message_id },
+ { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
+ );
+ setIsSearchOpen(false);
+ }}
+ onClose={() => setIsSearchOpen(false)}
+ />
+ )}
+ {isDigestOpen && (
+ {
+ navigateToAnchor(
+ { sessionId: activeSessionId, messageId },
+ { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
+ );
+ setIsDigestOpen(false);
+ }}
+ onClose={() => setIsDigestOpen(false)}
+ />
+ )}
+ {isExportOpen && (
+ setIsExportOpen(false),
+ sessionId: activeSessionId,
+ dayStack: chatWindow.displayDayStack,
+ }}
+ />
+ )}
+
+ {
+ navigateToAnchor(
+ { sessionId: activeSessionId, messageId: targetAnchor },
+ { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
+ );
+ }}
+ selectedCategory={chatWindow.selectedCategory}
+ unreadMessageIds={unreadMessageIds}
+ onSaveToPrivate={handleSaveMessageToPrivate}
+ onExportDocx={handleExportReplyDocx}
+ onOpenDoc={handleSelectDoc}
+ />
+ {
+ const targetId = await consumeNextUnread();
+ if (targetId && activeSessionId) {
+ navigateToAnchor(
+ { sessionId: activeSessionId, messageId: targetId, highlightMs: 2000 },
+ { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
+ );
+ }
+ }}
+ onNavigateToCategory={(_cat, lastMessageId) => {
+ if (lastMessageId && activeSessionId) {
+ const anchorId = resolveTurnAnchorId(lastMessageId, chatWindow.allMessages);
+ navigateToAnchor(
+ { sessionId: activeSessionId, messageId: anchorId, highlightMs: 2000 },
+ { sessionId: activeSessionId, loadDayForAnchor: chatWindow.loadDayForAnchor }
+ );
+ }
+ }}
+ onSelectCategory={chatWindow.setSelectedCategory}
+ />
+
+ {pendingProposal && (
+
+ {proposalError && (
+
+ {proposalError}
+
+ )}
+
+
+ )}
+
+
+ );
+}