From a20a9c8292d913402e7415551c0f624e32abb512 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 1 Dec 2025 21:22:34 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20persist=20status=20URL=20?= =?UTF-8?q?using=20storage.ts=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getStatusUrlKey to storage.ts with standard format - Register in PERSISTENT_WORKSPACE_KEY_FUNCTIONS for fork/delete - Add migrateWorkspaceStorage for rename support (fixes pre-existing bug) - Update StreamingMessageAggregator to use centralized key function Previously, status URLs used a non-standard key format and weren't migrated on workspace rename. Now all workspace storage keys are properly migrated when a workspace is renamed. _Generated with `mux`_ --- src/browser/contexts/WorkspaceContext.tsx | 9 ++++++--- .../messages/StreamingMessageAggregator.ts | 17 +++++------------ src/common/constants/storage.ts | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/browser/contexts/WorkspaceContext.tsx b/src/browser/contexts/WorkspaceContext.tsx index 74d8441aca..15ad6f1887 100644 --- a/src/browser/contexts/WorkspaceContext.tsx +++ b/src/browser/contexts/WorkspaceContext.tsx @@ -11,7 +11,7 @@ import { import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import type { WorkspaceSelection } from "@/browser/components/ProjectSidebar"; import type { RuntimeConfig } from "@/common/types/runtime"; -import { deleteWorkspaceStorage } from "@/common/constants/storage"; +import { deleteWorkspaceStorage, migrateWorkspaceStorage } from "@/common/constants/storage"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { useProjectContext } from "@/browser/contexts/ProjectContext"; import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore"; @@ -320,6 +320,11 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) { try { const result = await window.api.workspace.rename(workspaceId, newName); if (result.success) { + const newWorkspaceId = result.data.newWorkspaceId; + + // Migrate localStorage keys from old to new workspace ID + migrateWorkspaceStorage(workspaceId, newWorkspaceId); + // Backend has already updated the config - reload projects to get updated state await refreshProjects(); @@ -328,8 +333,6 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) { // Update selected workspace if it was renamed if (selectedWorkspace?.workspaceId === workspaceId) { - const newWorkspaceId = result.data.newWorkspaceId; - // Get updated workspace metadata from backend const newMetadata = await window.api.workspace.getInfo(newWorkspaceId); if (newMetadata) { diff --git a/src/browser/utils/messages/StreamingMessageAggregator.ts b/src/browser/utils/messages/StreamingMessageAggregator.ts index fcb824c195..66c23ada1e 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.ts @@ -31,6 +31,7 @@ import type { import { isDynamicToolPart } from "@/common/types/toolParts"; import { createDeltaStorage, type DeltaRecordStorage } from "./StreamingTPSCalculator"; import { computeRecencyTimestamp } from "./recency"; +import { getStatusUrlKey } from "@/common/constants/storage"; // Maximum number of messages to display in the DOM for performance // Full history is still maintained internally for token counting and stats @@ -125,18 +126,11 @@ export class StreamingMessageAggregator { this.updateRecency(); } - /** localStorage key for persisting lastStatusUrl. Only call when workspaceId is defined. */ - private getStatusUrlKey(): string | undefined { - if (!this.workspaceId) return undefined; - return `mux:workspace:${this.workspaceId}:lastStatusUrl`; - } - /** Load lastStatusUrl from localStorage */ private loadLastStatusUrl(): string | undefined { - const key = this.getStatusUrlKey(); - if (!key) return undefined; + if (!this.workspaceId) return undefined; try { - const stored = localStorage.getItem(key); + const stored = localStorage.getItem(getStatusUrlKey(this.workspaceId)); return stored ?? undefined; } catch { return undefined; @@ -148,10 +142,9 @@ export class StreamingMessageAggregator { * Once set, the URL can only be replaced with a new URL, never deleted. */ private saveLastStatusUrl(url: string): void { - const key = this.getStatusUrlKey(); - if (!key) return; + if (!this.workspaceId) return; try { - localStorage.setItem(key, url); + localStorage.setItem(getStatusUrlKey(this.workspaceId), url); } catch { // Ignore localStorage errors } diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index 4e5f5ee55d..8a0054e947 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -162,6 +162,15 @@ export function getFileTreeExpandStateKey(workspaceId: string): string { return `fileTreeExpandState:${workspaceId}`; } +/** + * Get the localStorage key for persisted status URL for a workspace + * Stores the last URL set via status_set tool (survives compaction) + * Format: "statusUrl:{workspaceId}" + */ +export function getStatusUrlKey(workspaceId: string): string { + return `statusUrl:${workspaceId}`; +} + /** * Get the localStorage key for unified Review search state per workspace * Stores: { input: string, useRegex: boolean, matchCase: boolean } @@ -202,6 +211,7 @@ const PERSISTENT_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> getFileTreeExpandStateKey, getReviewSearchStateKey, getAutoCompactionEnabledKey, + getStatusUrlKey, // Note: getAutoCompactionThresholdKey is per-model, not per-workspace ]; @@ -242,3 +252,12 @@ export function deleteWorkspaceStorage(workspaceId: string): void { localStorage.removeItem(key); } } + +/** + * Migrate all workspace-specific localStorage keys from old to new workspace ID + * Should be called when a workspace is renamed to preserve settings + */ +export function migrateWorkspaceStorage(oldWorkspaceId: string, newWorkspaceId: string): void { + copyWorkspaceStorage(oldWorkspaceId, newWorkspaceId); + deleteWorkspaceStorage(oldWorkspaceId); +}