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