返回 DeepSeek-Reasonix
composerProfile.ts
根目录 / desktop / frontend / src / lib / composerProfile.ts
1 import {
2 modeFromAxes,
3 modeHasAutoApproveTools,
4 modeHasPlan,
5 normalizeCollaborationMode,
6 normalizeMode,
7 normalizeTokenMode,
8 normalizeToolApprovalMode,
9 type CollaborationMode,
10 type GoalStatus,
11 type Meta,
12 type Mode,
13 type TabMeta,
14 type TokenMode,
15 type ToolApprovalMode,
16 } from "./types";
17
18 export type ComposerProfileField = "collaborationMode" | "toolApprovalMode" | "tokenMode" | "goal";
19
20 export type ComposerProfilePending = Partial<Record<ComposerProfileField, true>>;
21
22 export interface ComposerProfile {
23 collaborationMode: CollaborationMode;
24 goalDraftMode: boolean;
25 toolApprovalMode: ToolApprovalMode;
26 tokenMode: TokenMode;
27 goal: string;
28 pending: ComposerProfilePending;
29 }
30
31 export type ComposerProfilesByTab = Record<string, ComposerProfile>;
32 export type UserPlanModeIntents = Record<string, true>;
33
34 const profileFields: ComposerProfileField[] = ["collaborationMode", "toolApprovalMode", "tokenMode", "goal"];
35
36 export const defaultComposerProfile: ComposerProfile = Object.freeze({
37 collaborationMode: "normal",
38 goalDraftMode: false,
39 toolApprovalMode: "ask",
40 tokenMode: "full",
41 goal: "",
42 pending: {},
43 });
44
45 function activeGoal(goal?: string, status?: GoalStatus): string {
46 const trimmed = (goal ?? "").trim();
47 if (!trimmed) return "";
48 if (status && status !== "running") return "";
49 return trimmed;
50 }
51
52 function profileWithPending(profile: Omit<ComposerProfile, "pending">, pending: ComposerProfilePending = {}): ComposerProfile {
53 return { ...profile, pending };
54 }
55
56 function fallbackToolApprovalMode(rawMode: string | undefined, fallback?: ToolApprovalMode | null): ToolApprovalMode | undefined {
57 if ((rawMode ?? "").trim() !== "") return undefined;
58 return fallback === "auto" ? "auto" : undefined;
59 }
60
61 export function composerProfileFromTab(tab?: TabMeta | null, fallback?: ToolApprovalMode | null): ComposerProfile {
62 if (!tab) return { ...defaultComposerProfile, pending: {} };
63 const legacyMode = normalizeMode(tab.mode);
64 const goal = activeGoal(tab.goal, tab.goalStatus);
65 return profileWithPending({
66 collaborationMode: normalizeCollaborationMode(tab.collaborationMode, goal, legacyMode),
67 goalDraftMode: false,
68 toolApprovalMode: normalizeToolApprovalMode(
69 tab.toolApprovalMode,
70 legacyMode,
71 tab.toolApprovalMode === "yolo",
72 fallbackToolApprovalMode(tab.toolApprovalMode, fallback),
73 ),
74 tokenMode: normalizeTokenMode(tab.tokenMode),
75 goal,
76 });
77 }
78
79 export function composerProfileFromMeta(meta?: Meta | null, legacyMode?: Mode, fallback?: ToolApprovalMode | null): ComposerProfile {
80 if (!meta) return { ...defaultComposerProfile, pending: {} };
81 const fallbackMode = normalizeMode(legacyMode);
82 const goal = activeGoal(meta.goal, meta.goalStatus);
83 const toolApprovalMode = normalizeToolApprovalMode(
84 meta.toolApprovalMode,
85 fallbackMode,
86 meta.autoApproveTools ?? meta.bypass,
87 fallbackToolApprovalMode(meta.toolApprovalMode, fallback),
88 );
89 return profileWithPending({
90 collaborationMode: normalizeCollaborationMode(meta.collaborationMode, goal, fallbackMode),
91 goalDraftMode: false,
92 toolApprovalMode,
93 tokenMode: normalizeTokenMode(meta.tokenMode),
94 goal,
95 });
96 }
97
98 function fieldValue(profile: ComposerProfile, field: ComposerProfileField): string {
99 return profile[field];
100 }
101
102 function assignField(profile: ComposerProfile, field: ComposerProfileField, value: string) {
103 switch (field) {
104 case "collaborationMode":
105 profile.collaborationMode = value as CollaborationMode;
106 return;
107 case "toolApprovalMode":
108 profile.toolApprovalMode = value as ToolApprovalMode;
109 return;
110 case "tokenMode":
111 profile.tokenMode = value as TokenMode;
112 return;
113 case "goal":
114 profile.goal = value;
115 return;
116 }
117 }
118
119 function profilesEqual(a: ComposerProfile | undefined, b: ComposerProfile | undefined): boolean {
120 if (!a || !b) return a === b;
121 return a.collaborationMode === b.collaborationMode
122 && a.goalDraftMode === b.goalDraftMode
123 && a.toolApprovalMode === b.toolApprovalMode
124 && a.tokenMode === b.tokenMode
125 && a.goal === b.goal
126 && profileFields.every((field) => Boolean(a.pending[field]) === Boolean(b.pending[field]));
127 }
128
129 export function reconcileComposerProfile(current: ComposerProfile | undefined, backend: ComposerProfile): ComposerProfile {
130 if (!current) return { ...backend, pending: {} };
131
132 const pending: ComposerProfilePending = {};
133 const next: ComposerProfile = { ...backend, pending };
134
135 for (const field of profileFields) {
136 if (!current.pending[field]) continue;
137 if (fieldValue(current, field) === fieldValue(backend, field)) continue;
138 pending[field] = true;
139 assignField(next, field, fieldValue(current, field));
140 }
141
142 if (current.goalDraftMode && !backend.goal && !next.goal) {
143 next.goalDraftMode = true;
144 }
145 if (next.goal) {
146 next.goalDraftMode = false;
147 }
148
149 return next;
150 }
151
152 export function hydrateComposerProfilesFromTabs(current: ComposerProfilesByTab, tabs: TabMeta[]): ComposerProfilesByTab {
153 const next: ComposerProfilesByTab = {};
154 let changed = false;
155
156 for (const tab of tabs) {
157 const profile = reconcileComposerProfile(current[tab.id], composerProfileFromTab(tab, current[tab.id]?.toolApprovalMode));
158 next[tab.id] = profile;
159 if (!profilesEqual(current[tab.id], profile)) changed = true;
160 }
161
162 for (const id of Object.keys(current)) {
163 if (!next[id]) changed = true;
164 }
165
166 return changed ? next : current;
167 }
168
169 export function hydrateComposerProfileFromMeta(current: ComposerProfilesByTab, tabId: string, meta: Meta): ComposerProfilesByTab {
170 const previous = current[tabId];
171 const backend = composerProfileFromMeta(
172 meta,
173 previous ? composerProfileMode(previous) : undefined,
174 previous?.toolApprovalMode,
175 );
176 const profile = reconcileComposerProfile(previous, backend);
177 if (profilesEqual(previous, profile)) return current;
178 return { ...current, [tabId]: profile };
179 }
180
181 export function patchComposerProfile(
182 current: ComposerProfilesByTab,
183 tabId: string,
184 base: ComposerProfile | undefined,
185 patch: Partial<Omit<ComposerProfile, "pending">>,
186 pendingFields: ComposerProfileField[],
187 ): ComposerProfilesByTab {
188 const previous = current[tabId] ?? base ?? defaultComposerProfile;
189 const pending: ComposerProfilePending = { ...previous.pending };
190 for (const field of pendingFields) pending[field] = true;
191 const profile: ComposerProfile = {
192 ...previous,
193 ...patch,
194 pending,
195 };
196 if (profile.goal) {
197 profile.goalDraftMode = false;
198 }
199 if (profilesEqual(previous, profile)) return current;
200 return { ...current, [tabId]: profile };
201 }
202
203 export function composerProfileMode(profile: ComposerProfile): Mode {
204 return modeFromAxes(profile.collaborationMode === "plan", profile.toolApprovalMode === "yolo");
205 }
206
207 export function displayedComposerProfileCollaborationMode(profile: ComposerProfile): CollaborationMode {
208 if (profile.goalDraftMode) return "goal";
209 return profile.collaborationMode;
210 }
211
212 export function controllerComposerProfileCollaborationMode(profile: ComposerProfile): CollaborationMode {
213 const displayed = displayedComposerProfileCollaborationMode(profile);
214 return displayed === "goal" && !profile.goal ? "normal" : displayed;
215 }
216
217 export function composerProfileWithMode(mode: Mode): Partial<Omit<ComposerProfile, "pending">> {
218 return {
219 collaborationMode: modeHasPlan(mode) ? "plan" : "normal",
220 goalDraftMode: false,
221 toolApprovalMode: modeHasAutoApproveTools(mode) ? "yolo" : "ask",
222 goal: "",
223 };
224 }
225
226 export function updateUserPlanModeIntent(
227 current: UserPlanModeIntents,
228 tabId: string | null | undefined,
229 enabled: boolean,
230 ): UserPlanModeIntents {
231 if (!tabId) return current;
232 if (enabled) {
233 return current[tabId] ? current : { ...current, [tabId]: true };
234 }
235 if (!current[tabId]) return current;
236 const next = { ...current };
237 delete next[tabId];
238 return next;
239 }
240
241 export function pruneUserPlanModeIntents(current: UserPlanModeIntents, tabIds: Iterable<string>): UserPlanModeIntents {
242 const live = new Set(tabIds);
243 let changed = false;
244 const next: UserPlanModeIntents = {};
245 for (const tabId of Object.keys(current)) {
246 if (live.has(tabId)) {
247 next[tabId] = true;
248 } else {
249 changed = true;
250 }
251 }
252 return changed ? next : current;
253 }
254
255 export function shouldRestoreUserPlanMode(current: UserPlanModeIntents, tabId: string | null | undefined): boolean {
256 return Boolean(tabId && current[tabId]);
257 }
258
259 export function resolvePlanRestoreTabId(eventTabId: string | null | undefined, activeTabId: string | null | undefined): string | null {
260 return eventTabId || activeTabId || null;
261 }
262
263 export function shouldRestoreUserPlanModeForProfile(
264 current: UserPlanModeIntents,
265 tabId: string | null | undefined,
266 profile?: Pick<ComposerProfile, "goal"> | null,
267 ): boolean {
268 return shouldRestoreUserPlanMode(current, tabId) && !profile?.goal.trim();
269 }
270
270 lines TYPESCRIPT