Skip to content

Commit 63c2f62

Browse files
committed
feat(session-management): optimize provider merge with snapshot-based updates
Pass the frontend-side snapshot to UpdateCodexSessionProviders so the backend can update provider mappings in-place without a cold full-scan. The runtime bridge now takes priority over dev HTTP bridge on desktop. Also adds a loading spinner to the save button for better UX.
1 parent 6fd5445 commit 63c2f62

8 files changed

Lines changed: 419 additions & 54 deletions

File tree

frontend/src/features/session-management/SessionManagementFeature.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default function SessionManagementFeature({ workspace = 'codex' }: Sessio
9292
updateDraftValue,
9393
saveProviderMerge,
9494
} = useSessionManagementProviderMerge({
95+
snapshot,
9596
projects,
9697
unknownProviderLabel: copy.unknownProvider,
9798
loadFailedMessage: copy.loadFailed,

frontend/src/features/session-management/SessionManagementView.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ export function ProviderMergeModal({
686686
return (
687687
<div key={row.sourceKey} className="flex items-center gap-3 px-5 py-4">
688688
<div className="min-w-0 flex-1">
689-
<div className="truncate text-[0.8125rem] font-black uppercase tracking-tight">
689+
<div className="truncate text-[0.8125rem] font-black normal-case tracking-tight">
690690
{sourceLabel}
691691
</div>
692692
<div className="mt-0.5 text-[0.5rem] font-black uppercase tracking-[0.18em] text-[var(--text-muted)]">
@@ -743,9 +743,11 @@ export function ProviderMergeModal({
743743
type="button"
744744
onClick={onSave}
745745
disabled={saving}
746-
className="btn-swiss text-[0.5625rem] disabled:opacity-50"
746+
aria-busy={saving}
747+
className="btn-swiss inline-flex items-center gap-1.5 text-[0.5625rem] disabled:opacity-50"
747748
>
748-
{saving ? '保存中…' : '保存'}
749+
{saving ? <RefreshCw className="h-3 w-3 animate-spin" strokeWidth={2.5} aria-hidden="true" /> : null}
750+
<span>{saving ? '保存中…' : '保存'}</span>
749751
</button>
750752
</div>
751753
</div>

frontend/src/features/session-management/api.ts

Lines changed: 142 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,45 @@ interface SessionManagementRuntimeApp {
1414
GetCodexSessionManagementSnapshot?: () => Promise<unknown>;
1515
RefreshCodexSessionManagementSnapshot?: () => Promise<unknown>;
1616
GetCodexSessionDetail?: (sessionID: string) => Promise<unknown>;
17-
UpdateCodexSessionProviders?: (input: { projectID: string; mappings: Array<{ sourceProvider: string; targetProvider: string }> }) => Promise<unknown>;
17+
UpdateCodexSessionProviders?: (input: {
18+
projectID: string;
19+
mappings: Array<{ sourceProvider: string; targetProvider: string }>;
20+
snapshot?: RuntimeSessionManagementSnapshot;
21+
}) => Promise<unknown>;
22+
}
23+
24+
interface RuntimeSessionManagementSnapshot {
25+
projectCount: number;
26+
sessionCount: number;
27+
activeSessionCount: number;
28+
archivedSessionCount: number;
29+
lastScanAt: string;
30+
providerCounts: Record<string, number>;
31+
projects: Array<{
32+
id: string;
33+
name: string;
34+
providerCounts: Record<string, number>;
35+
sessionCount: number;
36+
activeSessionCount: number;
37+
archivedSessionCount: number;
38+
lastActiveAt: string;
39+
providerSummary: string;
40+
sessions: Array<{
41+
id: string;
42+
sessionID: string;
43+
projectID: string;
44+
projectName: string;
45+
title: string;
46+
status: string;
47+
archived: boolean;
48+
messageCount: number;
49+
roleSummary: string;
50+
updatedAt: string;
51+
fileLabel: string;
52+
summary: string;
53+
provider: string;
54+
}>;
55+
}>;
1856
}
1957

2058
declare global {
@@ -28,11 +66,21 @@ declare global {
2866
}
2967

3068
function resolveRuntimeMethod<T extends keyof SessionManagementRuntimeApp>(methodName: T) {
69+
const method = getRuntimeMethod(methodName);
70+
71+
if (!method) {
72+
throw new Error(`当前运行时缺少 ${methodName} 绑定。`);
73+
}
74+
75+
return method;
76+
}
77+
78+
function getRuntimeMethod<T extends keyof SessionManagementRuntimeApp>(methodName: T) {
3179
const app = globalThis.window?.go?.main?.App;
3280
const method = app?.[methodName];
3381

3482
if (typeof method !== 'function') {
35-
throw new Error(`当前运行时缺少 ${methodName} 绑定。`);
83+
return null;
3684
}
3785

3886
return method.bind(app) as NonNullable<SessionManagementRuntimeApp[T]>;
@@ -141,57 +189,141 @@ async function updateDevProviders(projectID: string, mappings: Array<{ sourcePro
141189
throw lastError ?? new Error('session management provider merge unavailable');
142190
}
143191

192+
function addProviderCount(counts: Record<string, number>, provider: string) {
193+
const key = provider.trim();
194+
if (!key || key === '—') {
195+
return;
196+
}
197+
counts[key] = (counts[key] ?? 0) + 1;
198+
}
199+
200+
function toRuntimeSessionManagementSnapshot(
201+
snapshot?: SessionManagementSnapshot,
202+
): RuntimeSessionManagementSnapshot | undefined {
203+
if (!snapshot) {
204+
return undefined;
205+
}
206+
207+
const providerCounts: Record<string, number> = {};
208+
const projects = snapshot.projects.map((project) => {
209+
const projectProviderCounts: Record<string, number> = {};
210+
const sessions = project.sessions.map((session) => {
211+
addProviderCount(providerCounts, session.provider);
212+
addProviderCount(projectProviderCounts, session.provider);
213+
return {
214+
id: session.id,
215+
sessionID: session.id,
216+
projectID: project.id,
217+
projectName: project.name,
218+
title: session.title,
219+
status: session.status,
220+
archived: session.status === 'archived',
221+
messageCount: session.messageCount,
222+
roleSummary: session.roleSummary,
223+
updatedAt: session.updatedAt,
224+
fileLabel: session.fileLabel,
225+
summary: session.summary,
226+
provider: session.provider,
227+
};
228+
});
229+
230+
return {
231+
id: project.id,
232+
name: project.name,
233+
providerCounts: projectProviderCounts,
234+
sessionCount: project.sessionCount,
235+
activeSessionCount: project.activeSessionCount,
236+
archivedSessionCount: project.archivedSessionCount,
237+
lastActiveAt: project.lastActiveAt,
238+
providerSummary: project.providerSummary,
239+
sessions,
240+
};
241+
});
242+
243+
return {
244+
projectCount: snapshot.stats.projectCount,
245+
sessionCount: snapshot.stats.sessionCount,
246+
activeSessionCount: snapshot.stats.activeSessionCount,
247+
archivedSessionCount: snapshot.stats.archivedSessionCount,
248+
lastScanAt: snapshot.stats.lastScanAt,
249+
providerCounts,
250+
projects,
251+
};
252+
}
253+
144254
export async function getCodexSessionManagementSnapshot(): Promise<SessionManagementSnapshot> {
145255
if (hasSessionManagementPreviewMode()) {
146256
return getSessionManagementPreviewSnapshot();
147257
}
258+
const getSnapshot = getRuntimeMethod('GetCodexSessionManagementSnapshot');
259+
if (getSnapshot) {
260+
const raw = await getSnapshot();
261+
return mapSessionManagementSnapshotResponse(raw);
262+
}
148263
if (canUseSessionManagementDevHTTP()) {
149264
return mapSessionManagementSnapshotResponse(await loadDevSnapshot(false));
150265
}
151266

152-
const getSnapshot = resolveRuntimeMethod('GetCodexSessionManagementSnapshot');
153-
const raw = await getSnapshot();
267+
const missingSnapshot = resolveRuntimeMethod('GetCodexSessionManagementSnapshot');
268+
const raw = await missingSnapshot();
154269
return mapSessionManagementSnapshotResponse(raw);
155270
}
156271

157272
export async function refreshCodexSessionManagementSnapshot(): Promise<SessionManagementSnapshot> {
158273
if (hasSessionManagementPreviewMode()) {
159274
return getSessionManagementPreviewSnapshot();
160275
}
276+
const refreshSnapshot = getRuntimeMethod('RefreshCodexSessionManagementSnapshot');
277+
if (refreshSnapshot) {
278+
const raw = await refreshSnapshot();
279+
return mapSessionManagementSnapshotResponse(raw);
280+
}
161281
if (canUseSessionManagementDevHTTP()) {
162282
return mapSessionManagementSnapshotResponse(await loadDevSnapshot(true));
163283
}
164284

165-
const refreshSnapshot = resolveRuntimeMethod('RefreshCodexSessionManagementSnapshot');
166-
const raw = await refreshSnapshot();
285+
const missingRefreshSnapshot = resolveRuntimeMethod('RefreshCodexSessionManagementSnapshot');
286+
const raw = await missingRefreshSnapshot();
167287
return mapSessionManagementSnapshotResponse(raw);
168288
}
169289

170290
export async function getCodexSessionDetail(sessionID: string): Promise<SessionDetail> {
171291
if (hasSessionManagementPreviewMode()) {
172292
return getSessionManagementPreviewDetail(sessionID);
173293
}
294+
const getDetail = getRuntimeMethod('GetCodexSessionDetail');
295+
if (getDetail) {
296+
const raw = await getDetail(sessionID);
297+
return mapSessionDetailResponse(raw);
298+
}
174299
if (canUseSessionManagementDevHTTP()) {
175300
return mapSessionDetailResponse(await loadDevDetail(sessionID));
176301
}
177302

178-
const getDetail = resolveRuntimeMethod('GetCodexSessionDetail');
179-
const raw = await getDetail(sessionID);
303+
const missingDetail = resolveRuntimeMethod('GetCodexSessionDetail');
304+
const raw = await missingDetail(sessionID);
180305
return mapSessionDetailResponse(raw);
181306
}
182307

183308
export async function updateCodexSessionProviders(
184309
projectID: string,
185310
mappings: Array<{ sourceProvider: string; targetProvider: string }>,
311+
snapshot?: SessionManagementSnapshot,
186312
): Promise<SessionManagementSnapshot> {
187313
if (hasSessionManagementPreviewMode()) {
188314
throw new Error('preview 模式不支持修改 provider');
189315
}
316+
const updateProviders = getRuntimeMethod('UpdateCodexSessionProviders');
317+
const runtimeSnapshot = toRuntimeSessionManagementSnapshot(snapshot);
318+
if (updateProviders) {
319+
const raw = await updateProviders({ projectID, mappings, snapshot: runtimeSnapshot });
320+
return mapSessionManagementSnapshotResponse(raw);
321+
}
190322
if (canUseSessionManagementDevHTTP()) {
191323
return mapSessionManagementSnapshotResponse(await updateDevProviders(projectID, mappings));
192324
}
193325

194-
const updateProviders = resolveRuntimeMethod('UpdateCodexSessionProviders');
195-
const raw = await updateProviders({ projectID, mappings });
326+
const missingUpdateProviders = resolveRuntimeMethod('UpdateCodexSessionProviders');
327+
const raw = await missingUpdateProviders({ projectID, mappings, snapshot: runtimeSnapshot });
196328
return mapSessionManagementSnapshotResponse(raw);
197329
}

0 commit comments

Comments
 (0)