From 409ab4444b3ed50de4f4061eb437cd80cd72d45b Mon Sep 17 00:00:00 2001 From: lyon Date: Fri, 26 Jun 2026 16:53:31 +0800 Subject: [PATCH] fix: refresh workbench projection across tabs --- web/hwlab-cloud-web/src/stores/workbench.ts | 46 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 79b8f4a8..b2bec40f 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -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>(); const sessionListRefreshTimers = new Map(); const sessionListLastRefreshAtByKey = new Map(); + const workbenchProjectionSignalSourceId = nextProtocolId("wbtab"); + let workbenchProjectionSignalChannel: BroadcastChannel | null = null; let workbenchReadHydrationActive = 0; const workbenchReadHydrationQueue: Array<() => void> = []; const workbenchReadCooldownUntilByKey = new Map(); @@ -602,11 +606,11 @@ export const useWorkbenchStore = defineStore("workbench", () => { rememberSessionMessages(id, mergeMessageProjectionPage(id, pageMessages)); } - async function refreshRealtimeSessionMessages(sessionId: string | null | undefined, reason: string): Promise { + async function refreshRealtimeSessionMessages(sessionId: string | null | undefined, reason: string, options: { force?: boolean } = {}): Promise { 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) => 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 }; });