返回 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 turnArgChars: runtime.turnArgChars, retry: runtime.retry,
61 turnOutputEstimated: runtime.turnOutputEstimated,
62 lastTurnOutputEstimated: runtime.lastTurnOutputEstimated,
63 readStatuses: runtime.readStatuses,
64 };
65 return {
66 runtime,
67 localToolsEnabled: !remoteActive,
68 composer: {
69 ...timing,
70 running: remote ? remote.running : local.running,
71 goalStatus: remote ? remote.composerProfile?.goalStatus : local.meta?.goalStatus,
72 goalView: remote ? remote.goalView : local.meta?.goalView,
73 goalRuntime: remote ? remote.goalRuntime : local.meta?.goalRuntime,
74 cwd: remote ? tab?.remote?.workspace : local.meta?.cwd,
75 modelLabel: modelLabel || connectingLabel,
76 commandCatalog: remote?.commands,
77 imageInputEnabled: !remoteActive && local.meta?.imageInputEnabled !== false,
78 imageUnderstandingEnabled: !remoteActive && local.meta?.visionFallbackEnabled === true,
79 attachmentInputEnabled: !remoteActive,
80 pinnedFiles: remote ? undefined : local.meta?.pinnedFiles,
81 turnId: remote ? undefined : local.activeTurnId,
82 effort: remote ? remote.effort : local.effort,
83 localDurableGuidance: !remoteActive,
84 context: runtime.context, turnCost: runtime.turnCost, turnRateBand: runtime.turnRateBand,
85 currency: runtime.sessionCurrency, cacheHitTokens: runtime.usage?.cacheHitTokens,
86 cacheMissTokens: runtime.usage?.cacheMissTokens, balance: runtime.balance,
87 },
88 context: {
89 tabId: remote ? undefined : activeTabId,
90 items: runtime.items, context: runtime.context, usage: runtime.usage,
91 sessionTokens: runtime.sessionTokens, sessionCost: runtime.sessionCost,
92 sessionCurrency: runtime.sessionCurrency, turnTokens: runtime.turnTotalTokens,
93 turnCost: runtime.turnCost, turnRateBand: runtime.turnRateBand, balance: runtime.balance,
94 sessionGen: runtime.sessionGen, usageSeq: runtime.usageSeq,
95 },
96 status: {
97 context: runtime.context, usage: runtime.usage, balance: runtime.balance,
98 running: runtime.running, jobs: runtime.jobs,
99 backgroundRuntimes: remote ? [] : backgroundRuntimes,
100 sessionTokens: runtime.sessionTokens, turnTokens: runtime.turnTotalTokens,
101 lastTurnOutputTokens: runtime.lastTurnOutputTokens, lastTurnModelMs: runtime.lastTurnModelMs,
102 lastTurnOutputEstimated: runtime.lastTurnOutputEstimated, lastRequestTps: runtime.lastRequestTps,
103 turnCost: runtime.turnCost, turnRateBand: runtime.turnRateBand, cost: runtime.sessionCost,
104 currency: runtime.sessionCurrency, modelLabel,
105 workspacePath: remote ? tab?.remote?.workspace : local.meta?.workspacePath || local.meta?.workspaceRoot || local.meta?.cwd,
106 workspaceName: remote ? tab?.workspaceName : local.meta?.workspaceName,
107 gitBranch: remote ? undefined : local.meta?.gitBranch,
108 },
109 };
110 }
111
112 export function projectConversationLayout(input: {
113 chatVisible: boolean; localToolsEnabled: boolean; dockMode: string;
114 dockRenderable: boolean; dockGridOpen: boolean; dockOverlay: boolean;
115 dockOpen: boolean; dockMaximized: boolean; terminalOpen: boolean;
116 }) {
117 const localDockBlocked = !input.localToolsEnabled && (input.dockMode === "files" || input.dockMode === "changed");
118 const dockVisible = input.chatVisible && input.dockRenderable && !localDockBlocked;
119 return {
120 dockVisible,
121 dockGridOpen: input.chatVisible && input.dockGridOpen && !localDockBlocked,
122 dockOverlay: dockVisible && input.dockOverlay,
123 dockMaximized: input.chatVisible && input.dockOpen && input.dockMaximized,
124 terminalOpen: input.chatVisible && input.terminalOpen && input.localToolsEnabled,
125 };
126 }
127
128 /** Workspace controller scope key: any identity input change re-scopes the composer. */
129 export function projectWorkspaceScopeKey(input: {
130 activeTabId: string | undefined;
131 sessionKey: string;
132 cwd: string | undefined;
133 sessionGen: number;
134 workspaceControllerEpoch: number;
135 }): string {
136 return [
137 input.activeTabId ?? "",
138 input.sessionKey,
139 input.cwd ?? "",
140 input.sessionGen,
141 input.workspaceControllerEpoch,
142 ].join("\u0000");
143 }
144
145 // Workspace navigation belongs to the project, not to a single conversation.
146 // A session switch inside the same project must therefore retain the dock,
147 // tree and selection state.
148 export function projectWorkspaceTreeMemoryKey(input: {
149 scope: string | undefined;
150 workspaceRoot: string | undefined;
151 cwd: string | undefined;
152 }): string {
153 return [
154 input.scope ?? "",
155 input.workspaceRoot ?? input.cwd ?? "",
156 ].join("\u0000");
157 }
158
158 lines TYPESCRIPT