fix: refresh workbench projection across tabs

This commit is contained in:
lyon
2026-06-26 16:53:31 +08:00
parent ec2acb739f
commit 409ab4444b
+44 -2
View File
@@ -29,6 +29,8 @@ const WORKBENCH_TURN_STATUS_MIN_REFRESH_MS = 2_000;
const WORKBENCH_TRACE_EVENTS_MIN_REFRESH_MS = 4_000;
const WORKBENCH_SESSION_MESSAGES_MIN_REFRESH_MS = 5_000;
const WORKBENCH_REALTIME_SESSION_MESSAGES_MIN_REFRESH_MS = 1_000;
const WORKBENCH_SESSION_PROJECTION_SIGNAL_CHANNEL = "hwlab.workbench.sessionProjection.v1";
const WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY = "hwlab.workbench.sessionProjectionSignal.v1";
const WORKBENCH_TRACE_EVENTS_TIMEOUT_MS = 5_000;
const SESSION_LIST_REALTIME_REFRESH_DELAY_MS = 5_000;
const SESSION_LIST_TERMINAL_REFRESH_DELAY_MS = 1_500;
@@ -86,6 +88,8 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const sessionListRefreshInFlight = new Map<string, Promise<void>>();
const sessionListRefreshTimers = new Map<string, number>();
const sessionListLastRefreshAtByKey = new Map<string, number>();
const workbenchProjectionSignalSourceId = nextProtocolId("wbtab");
let workbenchProjectionSignalChannel: BroadcastChannel | null = null;
let workbenchReadHydrationActive = 0;
const workbenchReadHydrationQueue: Array<() => void> = [];
const workbenchReadCooldownUntilByKey = new Map<string, number>();
@@ -602,11 +606,11 @@ export const useWorkbenchStore = defineStore("workbench", () => {
rememberSessionMessages(id, mergeMessageProjectionPage(id, pageMessages));
}
async function refreshRealtimeSessionMessages(sessionId: string | null | undefined, reason: string): Promise<void> {
async function refreshRealtimeSessionMessages(sessionId: string | null | undefined, reason: string, options: { force?: boolean } = {}): Promise<void> {
const id = normalizeWorkbenchSessionId(sessionId);
if (!id || id !== activeSessionId.value || realtimeSessionMessagesInFlight.has(id)) return;
const lastRefreshAt = realtimeSessionMessagesLastRefreshAtById.get(id) ?? 0;
if (lastRefreshAt > 0 && Date.now() - lastRefreshAt < WORKBENCH_REALTIME_SESSION_MESSAGES_MIN_REFRESH_MS) return;
if (!options.force && lastRefreshAt > 0 && Date.now() - lastRefreshAt < WORKBENCH_REALTIME_SESSION_MESSAGES_MIN_REFRESH_MS) return;
realtimeSessionMessagesLastRefreshAtById.set(id, Date.now());
realtimeSessionMessagesInFlight.add(id);
try {
@@ -852,6 +856,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
status: response.data.status ?? "running"
};
applyTurnStatusSnapshot(canonicalTraceId, response.data);
publishWorkbenchProjectionSignal(sessionId, canonicalTraceId, "submit-admitted");
scheduleSessionListRefresh(sessionId, SESSION_LIST_TERMINAL_REFRESH_DELAY_MS);
if ((response.data as AgentChatResultResponse).terminal === true || isTerminalMessageStatus(response.data.status)) {
completeTrace(canonicalTraceId, response.data as AgentChatResultResponse);
@@ -1309,6 +1314,42 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
});
}
function installWorkbenchProjectionSignalHandler(): void {
if (typeof window === "undefined") return;
if (typeof BroadcastChannel !== "undefined") {
workbenchProjectionSignalChannel = new BroadcastChannel(WORKBENCH_SESSION_PROJECTION_SIGNAL_CHANNEL);
workbenchProjectionSignalChannel.addEventListener("message", (event: MessageEvent<unknown>) => handleWorkbenchProjectionSignal(event.data));
}
window.addEventListener("storage", (event: StorageEvent) => {
if (event.key !== WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY || !event.newValue) return;
try {
handleWorkbenchProjectionSignal(JSON.parse(event.newValue));
} catch {
// Ignore malformed cross-tab payloads from older pages.
}
});
}
function publishWorkbenchProjectionSignal(sessionId: string | null | undefined, traceId: string | null | undefined, reason: string): void {
if (typeof window === "undefined") return;
const id = normalizeWorkbenchSessionId(sessionId);
if (!id) return;
const payload = { type: "session-projection", sourceId: workbenchProjectionSignalSourceId, sessionId: id, traceId: firstNonEmptyString(traceId) ?? null, reason, at: new Date().toISOString(), valuesRedacted: true };
try { workbenchProjectionSignalChannel?.postMessage(payload); } catch { /* ignore */ }
try { window.localStorage.setItem(WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY, JSON.stringify(payload)); } catch { /* ignore */ }
}
function handleWorkbenchProjectionSignal(value: unknown): void {
const record = recordValue(value);
if (!record || record.sourceId === workbenchProjectionSignalSourceId) return;
if (firstNonEmptyString(record.type) !== "session-projection") return;
const sessionId = normalizeWorkbenchSessionId(record.sessionId);
if (!sessionId || sessionId !== activeSessionId.value) return;
const traceId = firstNonEmptyString(record.traceId);
if (traceId && messages.value.some((message) => firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) === traceId)) return;
void refreshRealtimeSessionMessages(sessionId, `cross-tab-session-projection:${firstNonEmptyString(record.reason, traceId, "session") ?? "session"}`, { force: true });
}
function applyTraceSnapshot(traceId: string, snapshot: TraceSnapshot): void {
const trace = snapshotToRunnerTrace({
...snapshot,
@@ -1618,6 +1659,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
}
installRealtimeVisibilityHandler();
installWorkbenchProjectionSignalHandler();
return { sessions, messages, activeMessages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, sessionListLoading, sessionDetailLoading, sessionListLoadedCount, sessionListHasMore, sessionListNextCursor, sessionListLoadingMore, sessionListLoadMoreError, chatPending, error, activeSessionId, activeSessionSelectionSource, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectSession, selectSessionById, deleteCurrentSession, refreshSessions, loadMoreSessions, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearSessionMessages, recordActivity };
});