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 <bash@opencode.itc.local>
This commit is contained in:
bashrusakh
2026-06-23 22:18:44 +03:00
committed by GitHub
co-authored by Leonid Skorobogatyy
parent 5f3ef320d2
commit a25fc4c25a
5 changed files with 45 additions and 7 deletions
+2 -2
View File
@@ -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<MobileSessionsSheetProps> = ({ 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) {
@@ -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) {
@@ -70,7 +70,7 @@ import {
normalizePath,
} from './sidebar/utils';
import {
mergeSessionDirectoryMetadata,
mergeLiveSessionWithGlobalSession,
refreshGlobalSessions,
refreshGlobalSessionsForDirectories,
resolveGlobalSessionDirectory,
@@ -361,7 +361,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
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));
@@ -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<Session> & {
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');
});
});
@@ -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<string, Session[]> => {
const next = new Map<string, Session[]>();
for (const session of sessions) {