返回 DeepSeek-Reasonix
conversationProjection.ts
根目录 / desktop / frontend / src / app-runtime / conversationProjection.ts
1 import type { State } from "../lib/useController";
2 import type { RemoteSessionApi } from "../lib/useRemoteSession";
3 import type { BackgroundRuntimeView, TabMeta } from "../lib/types";
4 import { projectSessionAvailability } from "../lib/sessionAvailability";
5
6 type Input = {
7 local: State;
8 remote?: Pick<RemoteSessionApi, "transcript" | "running" | "modelLabel" | "commands" | "composerProfile" | "goalView" | "goalRuntime" | "effort">;
9 tab?: Pick<TabMeta, "id" | "label" | "remote" | "workspaceName">;
10 activeTabId?: string;
11 backgroundRuntimes: BackgroundRuntimeView[];
12 connectingLabel: string;
13 };
14
15 /** Readiness and paint identity must come from the surface that will render. */
16 export function projectNavigationSurfaceTarget(input: {
17 activeTabId?: string; sessionKey: string;
18 local: Pick<State, "meta" | "backendActivationPending" | "hydrating" | "hydrateError">;
19 remote?: Pick<RemoteSessionApi, "state" | "hydrated" | "error" | "surfaceGeneration">;
20 }) {
21 const { remote, local } = input;
22 const availability = projectSessionAvailability(input);
23 if (!remote) {
24 // Local history has its own controller-independent canonical read path.
25 // Navigation paint therefore waits for the readable history cut, not for
26 // provider/MCP/lease runtime readiness. Write actions remain fenced by
27 // controllerReady in the composer and command owners.
28 return {
29 activeTabId: input.activeTabId,
30 sessionKey: input.sessionKey,
31 ready: Boolean(input.activeTabId) && !local.hydrating && !local.hydrateError,
32 backendActivationPending: false,
33 hydrating: Boolean(local.hydrating),
34 hydrateError: local.hydrateError,
35 };
36 }
37 return {
38 activeTabId: input.activeTabId,
39 sessionKey: JSON.stringify([input.sessionKey, remote.surfaceGeneration]),
40 ready: availability.kind === "ready",
41 backendActivationPending: false,
42 hydrating: availability.kind === "loading",
43 hydrateError: availability.kind === "error" ? availability.detail || availability.source : undefined,
44 };
45 }
46
47 /** Display-only projection. No local telemetry fallback is permitted on a remote surface. */
48 export function projectConversation({ local, remote, tab, activeTabId, backgroundRuntimes, connectingLabel }: Input) {
49 const runtime = remote?.transcript ?? local;
50 const remoteActive = Boolean(remote);
51 const modelLabel = remote ? remote.modelLabel || tab?.label : local.meta?.label;
52 const timing = {
53 turnPhase: runtime.turnPhase, turnStartAt: runtime.turnStartAt,
54 turnDoneAt: runtime.turnDoneAt, lastTurnOutputTokens: runtime.lastTurnOutputTokens,
55 lastTurnWaitAccumMs: runtime.lastTurnWaitAccumMs,
56 turnWaitAccumMs: runtime.turnWaitAccumMs, promptWaitStartedAt: runtime.promptWaitStartedAt,
57 turnTokens: runtime.turnTokens, turnOutputTokens: runtime.turnOutputTokens,
58 turnOutputCharsAtUsage: runtime.turnOutputCharsAtUsage,
59 turnModelActiveAt: runtime.turnModelActiveAt, turnModelActiveMs: runtime.turnModelActiveMs,
60 turnRateOutputQuarters: runtime.turnRateSample?.outputQuarters,
61 turnArgChars: runtime.turnArgChars, retry: runtime.retry,
62 turnOutputEstimated: runtime.turnOutputEstimated,
63 lastTurnOutputEstimated: runtime.lastTurnOutputEstimated,
64 readStatuses: runtime.readStatuses,
65 };
66 return {
67 runtime,
68 localToolsEnabled: !remoteActive,
69 composer: {
70 ...timing,
71 running: remote ? remote.running : local.running,
72 goalStatus: remote ? remote.composerProfile?.goalStatus : local.meta?.goalStatus,
73 goalView: remote ? remote.goalView : local.meta?.goalView,
74 goalRuntime: remote ? remote.goalRuntime : local.meta?.goalRuntime,
75 cwd: remote ? tab?.remote?.workspace : local.meta?.cwd,
76 modelLabel: modelLabel || connectingLabel,
77 commandCatalog: remote?.commands,
78 imageInputEnabled: !remoteActive && local.meta?.imageInputEnabled !== false,
79 imageUnderstandingEnabled: !remoteActive && local.meta?.visionFallbackEnabled === true,
80 attachmentInputEnabled: !remoteActive,
81 pinnedFiles: remote ? undefined : local.meta?.pinnedFiles,
82 turnId: remote ? undefined : local.activeTurnId,
83 effort: remote ? remote.effort : local.effort,
84 localDurableGuidance: !remoteActive,
85 context: runtime.context, turnCost: runtime.turnCost, turnRateBand: runtime.turnRateBand,
86 currency: runtime.sessionCurrency, cacheHitTokens: runtime.usage?.cacheHitTokens,
87 cacheMissTokens: runtime.usage?.cacheMissTokens, balance: runtime.balance,
88 },
89 context: {
90 tabId: activeTabId,
91 items: runtime.items, context: runtime.context, usage: runtime.usage,
92 sessionTokens: runtime.sessionTokens, sessionCost: runtime.sessionCost,
93 sessionCurrency: runtime.sessionCurrency, turnTokens: runtime.turnTotalTokens,
94 turnCost: runtime.turnCost, turnRateBand: runtime.turnRateBand, balance: runtime.balance,
95 sessionGen: runtime.sessionGen, usageSeq: runtime.usageSeq,
96 },
97 status: {
98 context: runtime.context, usage: runtime.usage, balance: runtime.balance,
99 running: runtime.running, jobs: runtime.jobs,
100 backgroundRuntimes: remote ? [] : backgroundRuntimes,
101 sessionTokens: runtime.sessionTokens, turnTokens: runtime.turnTotalTokens,
102 lastTurnOutputTokens: runtime.lastTurnOutputTokens, lastTurnModelMs: runtime.lastTurnModelMs,
103 lastTurnOutputEstimated: runtime.lastTurnOutputEstimated, lastRequestTps: runtime.lastRequestTps,
104 turnCost: runtime.turnCost, turnRateBand: runtime.turnRateBand, cost: runtime.sessionCost,
105 currency: runtime.sessionCurrency, modelLabel,
106 workspacePath: remote ? tab?.remote?.workspace : local.meta?.workspacePath || local.meta?.workspaceRoot || local.meta?.cwd,
107 workspaceName: remote ? tab?.workspaceName : local.meta?.workspaceName,
108 gitBranch: remote ? undefined : local.meta?.gitBranch,
109 },
110 };
111 }
112
113 export function projectConversationLayout(input: {
114 chatVisible: boolean; localToolsEnabled: boolean; dockMode: string;
115 dockRenderable: boolean; dockGridOpen: boolean; dockOverlay: boolean;
116 dockOpen: boolean; dockMaximized: boolean; terminalOpen: boolean;
117 }) {
118 const localDockBlocked = !input.localToolsEnabled && (input.dockMode === "files" || input.dockMode === "changed");
119 const dockVisible = input.chatVisible && input.dockRenderable && !localDockBlocked;
120 return {
121 dockVisible,
122 dockGridOpen: input.chatVisible && input.dockGridOpen && !localDockBlocked,
123 dockOverlay: dockVisible && input.dockOverlay,
124 dockMaximized: input.chatVisible && input.dockOpen && input.dockMaximized,
125 terminalOpen: input.chatVisible && input.terminalOpen && input.localToolsEnabled,
126 };
127 }
128
129 /** Workspace controller scope key: any identity input change re-scopes the composer. */
130 export function projectWorkspaceScopeKey(input: {
131 activeTabId: string | undefined;
132 sessionKey: string;
133 cwd: string | undefined;
134 sessionGen: number;
135 workspaceControllerEpoch: number;
136 }): string {
137 return [
138 input.activeTabId ?? "",
139 input.sessionKey,
140 input.cwd ?? "",
141 input.sessionGen,
142 input.workspaceControllerEpoch,
143 ].join("\u0000");
144 }
145
146 // Workspace navigation belongs to the project, not to a single conversation.
147 // A session switch inside the same project must therefore retain the dock,
148 // tree and selection state.
149 export function projectWorkspaceTreeMemoryKey(input: {
150 scope: string | undefined;
151 workspaceRoot: string | undefined;
152 cwd: string | undefined;
153 }): string {
154 return [
155 input.scope ?? "",
156 input.workspaceRoot ?? input.cwd ?? "",
157 ].join("\u0000");
158 }
159
159 lines TYPESCRIPT