From a25fc4c25ac8e7f294846855fd38d21cb409a27e Mon Sep 17 00:00:00 2001 From: bashrusakh <127580858+bashrusakh@users.noreply.github.com> Date: Wed, 24 Jun 2026 06:18:44 +1100 Subject: [PATCH] fix(sync): reflect share status from global store after cancel (#1709) * fix(sync): reflect share status from global store after cancel Fix #1551: unshareSession() called updateLiveSession() which silently fails when the child store doesn't exist. The sidebar rendered from the child store first, showing stale share data. Now overlays the global session's share field at merge points. * fix(sync): extract shared mergeLiveSessionWithGlobalSession helper Extracted the share-field overlay into a single shared helper in useGlobalSessionsStore.ts. All 3 merge sites now use the helper instead of duplicating the overlay logic. * test(sync): add unit tests for mergeLiveSessionWithGlobalSession helper --------- Co-authored-by: Leonid Skorobogatyy --- packages/ui/src/apps/MobileSessionsSheet.tsx | 4 +-- .../chat/MobileSessionStatusBar.tsx | 4 +-- .../src/components/session/SessionSidebar.tsx | 4 +-- .../src/stores/useGlobalSessionsStore.test.ts | 29 ++++++++++++++++++- .../ui/src/stores/useGlobalSessionsStore.ts | 11 +++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/apps/MobileSessionsSheet.tsx b/packages/ui/src/apps/MobileSessionsSheet.tsx index 39449100..97587436 100644 --- a/packages/ui/src/apps/MobileSessionsSheet.tsx +++ b/packages/ui/src/apps/MobileSessionsSheet.tsx @@ -45,7 +45,7 @@ import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/pro import { cn } from '@/lib/utils'; import { listProjectWorktrees } from '@/lib/worktrees/worktreeManager'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; -import { mergeSessionDirectoryMetadata, refreshGlobalSessions, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; +import { mergeLiveSessionWithGlobalSession, refreshGlobalSessions, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { useMobileSessionExpansionStore } from '@/stores/useMobileSessionExpansionStore'; import { useMobileSessionTreeStore } from '@/stores/useMobileSessionTreeStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; @@ -632,7 +632,7 @@ export const MobileSessionsSheet: React.FC = ({ open, const liveById = new Map(liveSessions.map((session) => [session.id, session])); const merged = globalActiveSessions.map((session) => { const liveSession = liveById.get(session.id); - return liveSession ? mergeSessionDirectoryMetadata(liveSession, session) : session; + return liveSession ? mergeLiveSessionWithGlobalSession(liveSession, session) : session; }); const seenIds = new Set(merged.map((session) => session.id)); for (const session of liveSessions) { diff --git a/packages/ui/src/components/chat/MobileSessionStatusBar.tsx b/packages/ui/src/components/chat/MobileSessionStatusBar.tsx index 32baad4a..fe47a173 100644 --- a/packages/ui/src/components/chat/MobileSessionStatusBar.tsx +++ b/packages/ui/src/components/chat/MobileSessionStatusBar.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useAllSessionStatuses, useAllLiveSessions } from '@/sync/sync-context'; -import { mergeSessionDirectoryMetadata, useGlobalSessionsStore, ensureGlobalSessionsLoaded, refreshGlobalSessions } from '@/stores/useGlobalSessionsStore'; +import { mergeLiveSessionWithGlobalSession, useGlobalSessionsStore, ensureGlobalSessionsLoaded, refreshGlobalSessions } from '@/stores/useGlobalSessionsStore'; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import type { Session } from '@opencode-ai/sdk/v2'; @@ -37,7 +37,7 @@ function useAllProjectSessions(): Session[] { const liveById = new Map(liveSessions.map((session) => [session.id, session])); const merged = globalActiveSessions.map((session) => { const liveSession = liveById.get(session.id); - return liveSession ? mergeSessionDirectoryMetadata(liveSession, session) : session; + return liveSession ? mergeLiveSessionWithGlobalSession(liveSession, session) : session; }); const seen = new Set(merged.map((session) => session.id)); for (const session of liveSessions) { diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index 8b4edaf3..eefb1666 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -70,7 +70,7 @@ import { normalizePath, } from './sidebar/utils'; import { - mergeSessionDirectoryMetadata, + mergeLiveSessionWithGlobalSession, refreshGlobalSessions, refreshGlobalSessionsForDirectories, resolveGlobalSessionDirectory, @@ -361,7 +361,7 @@ export const SessionSidebar: React.FC = ({ const liveById = new Map(liveSessions.map((session) => [session.id, session])); const merged = globalActiveSessions.map((session) => { const liveSession = liveById.get(session.id); - return liveSession ? mergeSessionDirectoryMetadata(liveSession, session) : session; + return liveSession ? mergeLiveSessionWithGlobalSession(liveSession, session) : session; }); const seenIds = new Set(merged.map((session) => session.id)); diff --git a/packages/ui/src/stores/useGlobalSessionsStore.test.ts b/packages/ui/src/stores/useGlobalSessionsStore.test.ts index 9c829cbd..3aaaa6d0 100644 --- a/packages/ui/src/stores/useGlobalSessionsStore.test.ts +++ b/packages/ui/src/stores/useGlobalSessionsStore.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, test } from 'bun:test'; import type { Session } from '@opencode-ai/sdk/v2'; -import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from './useGlobalSessionsStore'; +import { resolveGlobalSessionDirectory, mergeLiveSessionWithGlobalSession, useGlobalSessionsStore } from './useGlobalSessionsStore'; type SessionExtra = Partial & { directory?: string | null; @@ -79,3 +79,30 @@ describe('useGlobalSessionsStore', () => { expect(resolveGlobalSessionDirectory(useGlobalSessionsStore.getState().archivedSessions[0])).toBe('/repo/app'); }); }); + +describe('mergeLiveSessionWithGlobalSession', () => { + test('preserves global share over live share', () => { + const live = buildSession('https://live.example/s', { time: { created: 1, updated: 5 } }); + const global = buildSession('https://global.example/s', { time: { created: 1, updated: 3 } }); + + const merged = mergeLiveSessionWithGlobalSession(live, global); + expect(merged.share?.url).toBe('https://global.example/s'); + expect(merged.time?.updated).toBe(5); + }); + + test('preserves directory from global when live omits it', () => { + const live = buildSession('https://live.example/s', { time: { created: 1, updated: 5 } }); + const global = buildSession('https://global.example/s', { directory: '/repo/app' }); + + const merged = mergeLiveSessionWithGlobalSession(live, global); + expect(resolveGlobalSessionDirectory(merged)).toBe('/repo/app'); + }); + + test('live directory takes precedence over global when present', () => { + const live = buildSession('https://live.example/s', { directory: '/repo/worktree' }); + const global = buildSession('https://global.example/s', { directory: '/repo/app' }); + + const merged = mergeLiveSessionWithGlobalSession(live, global); + expect(resolveGlobalSessionDirectory(merged)).toBe('/repo/worktree'); + }); +}); diff --git a/packages/ui/src/stores/useGlobalSessionsStore.ts b/packages/ui/src/stores/useGlobalSessionsStore.ts index 0fa8d101..354c76c0 100644 --- a/packages/ui/src/stores/useGlobalSessionsStore.ts +++ b/packages/ui/src/stores/useGlobalSessionsStore.ts @@ -100,6 +100,17 @@ export const mergeSessionDirectoryMetadata = (incoming: Session, existing?: Sess return changed ? next : incoming; }; +export const mergeLiveSessionWithGlobalSession = ( + liveSession: Session, + globalSession: Session, +): Session => { + const merged = mergeSessionDirectoryMetadata(liveSession, globalSession); + if (merged.share !== globalSession.share) { + return { ...merged, share: globalSession.share }; + } + return merged; +}; + const buildSessionsByDirectory = (sessions: Session[]): Map => { const next = new Map(); for (const session of sessions) {