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:
committed by
GitHub
co-authored by
Leonid Skorobogatyy
parent
5f3ef320d2
commit
a25fc4c25a
@@ -45,7 +45,7 @@ import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/pro
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { listProjectWorktrees } from '@/lib/worktrees/worktreeManager';
|
import { listProjectWorktrees } from '@/lib/worktrees/worktreeManager';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
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 { useMobileSessionExpansionStore } from '@/stores/useMobileSessionExpansionStore';
|
||||||
import { useMobileSessionTreeStore } from '@/stores/useMobileSessionTreeStore';
|
import { useMobileSessionTreeStore } from '@/stores/useMobileSessionTreeStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
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 liveById = new Map(liveSessions.map((session) => [session.id, session]));
|
||||||
const merged = globalActiveSessions.map((session) => {
|
const merged = globalActiveSessions.map((session) => {
|
||||||
const liveSession = liveById.get(session.id);
|
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));
|
const seenIds = new Set(merged.map((session) => session.id));
|
||||||
for (const session of liveSessions) {
|
for (const session of liveSessions) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useAllSessionStatuses, useAllLiveSessions } from '@/sync/sync-context';
|
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 { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
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 liveById = new Map(liveSessions.map((session) => [session.id, session]));
|
||||||
const merged = globalActiveSessions.map((session) => {
|
const merged = globalActiveSessions.map((session) => {
|
||||||
const liveSession = liveById.get(session.id);
|
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));
|
const seen = new Set(merged.map((session) => session.id));
|
||||||
for (const session of liveSessions) {
|
for (const session of liveSessions) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import {
|
|||||||
normalizePath,
|
normalizePath,
|
||||||
} from './sidebar/utils';
|
} from './sidebar/utils';
|
||||||
import {
|
import {
|
||||||
mergeSessionDirectoryMetadata,
|
mergeLiveSessionWithGlobalSession,
|
||||||
refreshGlobalSessions,
|
refreshGlobalSessions,
|
||||||
refreshGlobalSessionsForDirectories,
|
refreshGlobalSessionsForDirectories,
|
||||||
resolveGlobalSessionDirectory,
|
resolveGlobalSessionDirectory,
|
||||||
@@ -361,7 +361,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const liveById = new Map(liveSessions.map((session) => [session.id, session]));
|
const liveById = new Map(liveSessions.map((session) => [session.id, session]));
|
||||||
const merged = globalActiveSessions.map((session) => {
|
const merged = globalActiveSessions.map((session) => {
|
||||||
const liveSession = liveById.get(session.id);
|
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));
|
const seenIds = new Set(merged.map((session) => session.id));
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, test } from 'bun:test';
|
import { beforeEach, describe, expect, test } from 'bun:test';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
import type { Session } from '@opencode-ai/sdk/v2';
|
||||||
|
|
||||||
import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from './useGlobalSessionsStore';
|
import { resolveGlobalSessionDirectory, mergeLiveSessionWithGlobalSession, useGlobalSessionsStore } from './useGlobalSessionsStore';
|
||||||
|
|
||||||
type SessionExtra = Partial<Session> & {
|
type SessionExtra = Partial<Session> & {
|
||||||
directory?: string | null;
|
directory?: string | null;
|
||||||
@@ -79,3 +79,30 @@ describe('useGlobalSessionsStore', () => {
|
|||||||
expect(resolveGlobalSessionDirectory(useGlobalSessionsStore.getState().archivedSessions[0])).toBe('/repo/app');
|
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;
|
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 buildSessionsByDirectory = (sessions: Session[]): Map<string, Session[]> => {
|
||||||
const next = new Map<string, Session[]>();
|
const next = new Map<string, Session[]>();
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
|||||||
Reference in New Issue
Block a user