返回 DeepSeek-Reasonix
ChatPaneRegion.tsx
根目录 / desktop / frontend / src / app-shell / ChatPaneRegion.tsx
1 import { ErrorMessage } from "../components/ErrorMessage";
2 import { lazy, Suspense, type ReactNode } from "react";
3 import { Transcript, type TranscriptProps } from "../components/Transcript";
4 import { SessionRecoveryBanner, SessionRecoveryPlaceholder } from "../components/SessionRecoveryBanner";
5 import { SessionLoadingIndicator } from "../components/SessionLoadingIndicator";
6 import { NoticePreviewPanel, noticePreviewMockEnabled } from "./NoticePreviewPanel";
7 import type { SidebarImConnection } from "../app-runtime/sidebarImProjection";
8 import type { TabMeta } from "../lib/types";
9 import type { State } from "../lib/useController";
10 import type { RemoteSessionApi } from "../lib/useRemoteSession";
11 import type { Translator } from "../lib/i18n";
12 import type { ForkBlockReason } from "../lib/forkTargets";
13 import type { SessionAvailability } from "../lib/sessionAvailability";
14 import { orderedLocalSubmissions } from "../lib/localSubmissionState";
15 import { RotateCcw } from "lucide-react";
16 import type { SessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
17 import { draftSurfaceNeedsAttention, sameDraftError } from "./draftPresentation";
18
19 const RemoteSessionSurface = lazy(() => import("../components/RemoteSessionSurface").then((module) => ({ default: module.RemoteSessionSurface })));
20 const SidebarImConnectionDetail = lazy(() => import("./SidebarImConnectionDetail").then((module) => ({ default: module.SidebarImConnectionDetail })));
21
22 export type ChatPaneTranscriptInput = {
23 state: State;
24 items: TranscriptProps["items"];
25 tabId: TranscriptProps["tabId"];
26 geometrySessionKey: TranscriptProps["geometrySessionKey"];
27 footerHeight: TranscriptProps["footerHeight"];
28 invocationMetadata: TranscriptProps["invocationMetadata"];
29 surfaceCommitToken: TranscriptProps["surfaceCommitToken"];
30 liveStore: TranscriptProps["liveStore"];
31 transcriptHydrating: boolean;
32 navigationDataReady: boolean;
33 readOnly: boolean;
34 controllerReady: boolean;
35 hydratePlaceholderActive: boolean;
36 clearContextPending: boolean;
37 emptyHero?: boolean;
38 availability: SessionAvailability;
39 rewind: {
40 stateActive: boolean;
41 committing: boolean;
42 };
43 };
44
45 export type ChatPaneRegionProps = {
46 transitioning: boolean;
47 t: Translator;
48 imDetail: {
49 connection: SidebarImConnection;
50 onClose: () => void;
51 onOpenSettings: () => void;
52 onManageAllowlist: (connectionId: string) => void;
53 onOpenSession: (connection: SidebarImConnection) => void;
54 } | null;
55 remote: { tab: TabMeta; session: RemoteSessionApi } | undefined;
56 draft?: {
57 surface: SessionDraftSurface;
58 onUseSaved(): void;
59 onKeepLocal(): void;
60 onRetrySave(): void;
61 onDismissTaskError?(): void;
62 onResume(): void;
63 onOpenSession(): void;
64 onCheckSubmission(): void;
65 };
66 /** Floating dock launcher card, mounted over the transcript's right edge. */
67 launcher?: ReactNode;
68 transcript: ChatPaneTranscriptInput;
69 onRetryHistory: () => Promise<unknown>;
70 commands: {
71 onPrompt: TranscriptProps["onPrompt"];
72 onFork: TranscriptProps["onFork"];
73 onOpenTurnChanges?: TranscriptProps["onOpenTurnChanges"];
74 onLoadOlderHistory: TranscriptProps["onLoadOlderHistory"];
75 onLoadNewerHistory: TranscriptProps["onLoadNewerHistory"];
76 onNavigateToTurn?: TranscriptProps["onNavigateToTurn"];
77 onSurfacePaintReady: TranscriptProps["onSurfacePaintReady"];
78 };
79 };
80
81 /**
82 * The chat-pane main surface: IM/bot detail, notice preview mock, remote
83 * session surface or the local transcript with its navigation-transition
84 * wrapper and history-load error. Pure prop-driven; all ownership stays in
85 * the caller's owners.
86 */
87 export function ChatPaneRegion(props: ChatPaneRegionProps) {
88 const { transitioning, t, transcript, commands } = props;
89 const { state, rewind } = transcript;
90 // A fork entry reads persisted turn records, so it never waits for the session
91 // to stop running, and a read-only source still forks: the child is written
92 // from the source, never into it. It does wait for the surface it belongs to:
93 // while the transcript hydrates or the source identity is switching, the
94 // records on screen are not yet the ones a cut would address.
95 const forkBlocked: ForkBlockReason | null = state.forkCreating ? "creating"
96 : !transcript.controllerReady || transcript.transcriptHydrating || transcript.hydratePlaceholderActive || transitioning
97 ? "loading"
98 : null;
99 const noticePreview = noticePreviewMockEnabled();
100 if (props.draft && !props.imDetail && !noticePreview) {
101 const draft = props.draft.surface;
102 if (!draftSurfaceNeedsAttention(draft)) {
103 return <main className="main main--draft-landing" aria-label={t("draft.surfaceLabel")} />;
104 }
105 const operationUnknown = draft.operation?.phase === "dispatch_unknown";
106 const operationError = draft.submissionError || (draft.operation && ["terminal_failed", "runtime_failed", "resume_required", "dispatch_unknown", "dispatching_shell"].includes(draft.operation.phase)
107 ? draft.operation.error
108 : "");
109 return <main className="main main--draft-attention">
110 <section className="session-draft-attention" aria-label={t("draft.surfaceLabel")} role="alert">
111 <div className={`session-draft-surface__status session-draft-surface__status--${draft.saveState}`} role="status">
112 {draft.operation?.phase === "accepted" ? t("draft.openSession") : operationUnknown ? t("draft.resultUnknown")
113 : draft.saveState === "error" ? t("draft.saveFailed")
114 : draft.saveState === "conflict" ? t("draft.conflict")
115 : draft.resumingSubmission ? t("draft.starting") : operationError ? t("draft.startFailed") : t("draft.starting")}
116 </div>
117 {draft.saveState === "conflict" ? <div className="session-draft-surface__conflict" role="alert">
118 <span>{t("draft.conflictDetail")}</span>
119 <button type="button" onClick={props.draft.onUseSaved}><RotateCcw size={14} />{t("draft.useSaved")}</button>
120 <button type="button" onClick={props.draft.onKeepLocal}>{t("draft.keepLocal")}</button>
121 </div> : null}
122 {draft.error ? <p className="session-draft-surface__error"><ErrorMessage error={draft.error} /> <button type="button" onClick={props.draft.onRetrySave}>{t("draft.retrySave")}</button></p> : null}
123 {operationError ? <p className="session-draft-surface__error"><ErrorMessage error={operationError} /></p> : null}
124 {draft.taskError && !sameDraftError(draft.taskError, operationError) && !sameDraftError(draft.taskError, draft.error) ? <p className="session-draft-surface__error"><ErrorMessage error={draft.taskError} /> <button type="button" onClick={props.draft.onDismissTaskError}>{t("common.close")}</button></p> : null}
125 {draft.operation?.canResume ? <button type="button" disabled={draft.resumingSubmission} onClick={props.draft.onResume}>{t("draft.resume")}</button> : null}
126 {operationUnknown ? <button type="button" onClick={props.draft.onCheckSubmission}>{t("draft.checkSubmission")}</button> : null}
127 {draft.operation?.phase === "accepted" ? <button type="button" onClick={props.draft.onOpenSession}>{t("draft.openSession")}</button> : null}
128 </section>
129 </main>;
130 }
131 if (props.remote && !(props.imDetail && !transitioning) && !noticePreview) {
132 return <Suspense fallback={null}><RemoteSessionSurface tab={props.remote.tab} session={props.remote.session}
133 surfaceCommitToken={transcript.surfaceCommitToken} onSurfacePaintReady={commands.onSurfacePaintReady} /></Suspense>;
134 }
135 const localSubmissions = orderedLocalSubmissions(state);
136 const recoveringEmpty = !transitioning && transcript.availability.kind !== "ready" && transcript.items.length === 0 && localSubmissions.length === 0
137 && !state.live?.text && !state.live?.reasoning;
138 const loading = transcript.availability.kind !== "error" && (transitioning || transcript.transcriptHydrating
139 || (transcript.availability.kind === "loading" && (transcript.availability.source !== "runtime" || recoveringEmpty)));
140 return (
141 <>
142 {!transitioning && !props.imDetail && !noticePreview && <SessionRecoveryBanner key={transcript.geometrySessionKey ?? transcript.tabId}
143 availability={transcript.availability} onRetry={props.onRetryHistory} />}
144 <main className="main">
145 {props.imDetail && !transitioning ? (
146 <SidebarImConnectionDetail
147 connection={props.imDetail.connection}
148 onClose={props.imDetail.onClose}
149 onOpenSettings={props.imDetail.onOpenSettings}
150 onManageAllowlist={() => props.imDetail!.onManageAllowlist(props.imDetail!.connection.connectionId)}
151 onOpenSession={() => props.imDetail!.onOpenSession(props.imDetail!.connection)}
152 />
153 ) : noticePreview ? (
154 <NoticePreviewPanel />
155 ) : (
156 <>
157 <div className="transcript-navigation-surface" aria-busy={loading}>
158 {props.launcher}
159 <div
160 className="transcript-navigation-content"
161 aria-hidden={transitioning || undefined}
162 ref={(node) => {
163 if (!node) return;
164 (node as HTMLElement & { inert?: boolean }).inert = transitioning;
165 }}
166 >
167 {recoveringEmpty ? <SessionRecoveryPlaceholder availability={transcript.availability} /> : <Transcript
168 items={transcript.items}
169 localSubmissions={localSubmissions}
170 localSubmissionSendRevision={state.localSubmissionSendRevision}
171 visibleSubmissionHandoffs={state.visibleSubmissionHandoffs}
172 live={transitioning ? undefined : state.live}
173 liveStore={transcript.liveStore}
174 tabId={transcript.tabId}
175 geometrySessionKey={transcript.geometrySessionKey}
176 footerHeight={transcript.footerHeight}
177 onPrompt={commands.onPrompt}
178 onFork={commands.onFork}
179 onOpenTurnChanges={commands.onOpenTurnChanges}
180 forkTargets={state.forkTargets}
181 forkBlocked={forkBlocked}
182 running={(state.running && !state.runtimeStateSnapshot?.maintenance) || rewind.committing}
183 turnStartAt={state.turnStartAt}
184 hydrating={transcript.transcriptHydrating || (transitioning && !transcript.navigationDataReady)}
185 showLoadingFeedback={false}
186 hasOlderHistory={!transitioning && state.historyHasOlder && !rewind.stateActive}
187 hasNewerHistory={!transitioning && state.historyHasNewer && !rewind.stateActive}
188 historyStartTurn={state.historyStartTurn}
189 historyEndTurn={state.historyEndTurn}
190 totalTurns={state.historyTotalTurns}
191 loadingOlderHistory={state.historyOlderLoading}
192 olderHistoryError={state.historyOlderError}
193 loadingNewerHistory={state.historyNewerLoading}
194 newerHistoryError={state.historyNewerError}
195 onLoadOlderHistory={commands.onLoadOlderHistory}
196 onLoadNewerHistory={commands.onLoadNewerHistory}
197 onNavigateToTurn={commands.onNavigateToTurn}
198 invocationMetadata={transcript.invocationMetadata}
199 surfaceCommitToken={transcript.surfaceCommitToken}
200 onSurfacePaintReady={commands.onSurfacePaintReady}
201 />}
202 </div>
203 {transitioning && <div className="transcript-navigation-overlay" aria-hidden="true" />}
204 <SessionLoadingIndicator active={loading}
205 identity={transcript.geometrySessionKey ?? transcript.tabId ?? "local"}
206 source={transcript.availability.source} />
207 </div>
208 </>
209 )}
210 </main>
211 </>
212 );
213 }
214
214 lines Plain Text