fix: refresh session share state after unsharing

Updates live session state after share and unshare actions
Preserves session directory metadata while applying share changes
Adds regression coverage for unshare and sanitized share responses
This commit is contained in:
Bohdan Triapitsyn
2026-06-07 09:09:51 +03:00
parent 78c851f7cd
commit 26eb89e0ce
2 changed files with 159 additions and 5 deletions
+132 -1
View File
@@ -8,6 +8,8 @@ const scopedClientDirectories: string[] = []
const registeredSessionDirectories: Array<{ sessionID: string; directory: string }> = []
let sessionRevertResult: { data?: unknown; error?: unknown; response?: { status?: number } } = {}
let questionReplyError: unknown | null = null
let sessionShareResult: { data?: unknown; error?: unknown; response?: { status?: number } } = {}
const globalUpsertedSessions: unknown[] = []
const mockScopedClient = {
permission: {
@@ -45,6 +47,14 @@ const mockSdk = {
replyCalls.push({ method: "session.abort", params })
return Promise.resolve({ data: true })
}),
share: mock((params: Record<string, unknown>) => {
replyCalls.push({ method: "session.share", params })
return Promise.resolve(sessionShareResult)
}),
unshare: mock((params: Record<string, unknown>) => {
replyCalls.push({ method: "session.unshare", params })
return Promise.resolve(sessionShareResult)
}),
},
permission: {
reply: mock((params: Record<string, unknown>) => {
@@ -141,9 +151,21 @@ mock.module("./input-store", () => ({
}))
mock.module("@/stores/useGlobalSessionsStore", () => ({
mergeSessionDirectoryMetadata: (incoming: Session, existing?: SessionWithDirectory | null): SessionWithDirectory => {
if (!existing) return incoming as SessionWithDirectory
const next = { ...(incoming as SessionWithDirectory) }
if (!next.directory && existing.directory) next.directory = existing.directory
if (!next.project && existing.project) next.project = existing.project
if (next.project && !next.project.worktree && existing.project?.worktree) {
next.project = { ...next.project, worktree: existing.project.worktree }
}
return next
},
useGlobalSessionsStore: {
getState: () => ({
upsertSession: () => {},
upsertSession: (session: unknown) => {
globalUpsertedSessions.push(session)
},
}),
},
}))
@@ -161,6 +183,10 @@ import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2
type OptimisticAddCall = { sessionID: string; directory?: string | null; message: Message; parts: Part[] }
type OptimisticRemoveCall = { sessionID: string; directory?: string | null; messageID: string }
type SessionWithDirectory = Session & {
directory?: string | null
project?: { worktree?: string | null }
}
function createStore(
permissions: Record<string, PermissionRequest[]>,
@@ -183,9 +209,114 @@ function createChildStores(entries: Array<[string, StoreApi<DirectoryStore>]>) {
if (!store) throw new Error(`No store for ${dir}`)
return store
},
getChild: (dir: string) => new Map(entries).get(dir),
} as unknown as import("./child-store").ChildStoreManager
}
describe("shareSession live state", () => {
beforeEach(() => {
replyCalls.length = 0
globalUpsertedSessions.length = 0
sessionShareResult = {}
})
test("updates the directory live store after unsharing", async () => {
const sharedSession = { id: "session-a", time: { created: 1 }, share: { url: "https://share.example/a" } } as Session
const unsharedSession = { id: "session-a", time: { created: 1, updated: 2 } } as Session
const sessionStore = createStore({}, { session: [sharedSession] })
const otherStore = createStore({}, { session: [{ id: "other", time: { created: 1 } } as Session] })
const childStores = createChildStores([
["/test/project", sessionStore],
["/other/project", otherStore],
])
sessionShareResult = { data: unsharedSession }
const { setActionRefs, unshareSession } = await import("./session-actions")
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
const result = await unshareSession("session-a")
expect(result).toBe(unsharedSession)
expect(replyCalls.find((call) => call.method === "session.unshare")?.params.directory).toBe("/test/project")
expect(sessionStore.getState().session[0].share).toBe(undefined)
expect(otherStore.getState().session[0].id).toBe("other")
expect(globalUpsertedSessions).toEqual([unsharedSession])
})
test("updates the directory live store after sharing", async () => {
const unsharedSession = { id: "session-a", time: { created: 1 } } as Session
const sharedSession = { id: "session-a", time: { created: 1, updated: 2 }, share: { url: "https://share.example/a" } } as Session
const sessionStore = createStore({}, { session: [unsharedSession] })
const childStores = createChildStores([["/test/project", sessionStore]])
sessionShareResult = { data: sharedSession }
const { setActionRefs, shareSession } = await import("./session-actions")
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
const result = await shareSession("session-a")
expect(result).toBe(sharedSession)
expect(replyCalls.find((call) => call.method === "session.share")?.params.directory).toBe("/test/project")
expect(sessionStore.getState().session[0].share?.url).toBe("https://share.example/a")
expect(globalUpsertedSessions).toEqual([sharedSession])
})
test("preserves live directory metadata while clearing share from null response", async () => {
const sharedSession = {
id: "session-a",
time: { created: 1 },
directory: "/test/project",
project: { worktree: "/test/project" },
share: { url: "https://share.example/a" },
} as SessionWithDirectory
const unsharedSession = {
id: "session-a",
time: { created: 1, updated: 2 },
share: null,
} as unknown as Session
const sessionStore = createStore({}, { session: [sharedSession] })
const childStores = createChildStores([["/test/project", sessionStore]])
sessionShareResult = { data: unsharedSession }
const { setActionRefs, unshareSession } = await import("./session-actions")
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
await unshareSession("session-a")
const liveSession = sessionStore.getState().session[0] as SessionWithDirectory & { share?: null }
expect(liveSession.share).toBe(null)
expect(liveSession.directory).toBe("/test/project")
expect(liveSession.project?.worktree).toBe("/test/project")
})
test("strips oversized diff snapshots before updating session stores", async () => {
const sessionWithDiff = {
id: "session-a",
time: { created: 1, updated: 2 },
share: { url: "https://share.example/a" },
summary: {
diffs: [{ file: "a.txt", before: "old", after: "new", additions: 1, deletions: 1 }],
},
} as unknown as Session
const sessionStore = createStore({}, { session: [{ id: "session-a", time: { created: 1 } } as Session] })
const childStores = createChildStores([["/test/project", sessionStore]])
sessionShareResult = { data: sessionWithDiff }
const { setActionRefs, shareSession } = await import("./session-actions")
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
const result = await shareSession("session-a")
const storedDiff = ((sessionStore.getState().session[0] as { summary?: { diffs?: Array<Record<string, unknown>> } }).summary?.diffs ?? [])[0]
const globalDiff = (((globalUpsertedSessions[0] as { summary?: { diffs?: Array<Record<string, unknown>> } }).summary?.diffs ?? [])[0])
const resultDiff = ((result as { summary?: { diffs?: Array<Record<string, unknown>> } }).summary?.diffs ?? [])[0]
expect(storedDiff.before).toBe(undefined)
expect(storedDiff.after).toBe(undefined)
expect(globalDiff.before).toBe(undefined)
expect(resultDiff.after).toBe(undefined)
})
})
describe("optimisticSend target directory", () => {
beforeEach(() => {
replyCalls.length = 0
+27 -4
View File
@@ -9,12 +9,12 @@ import { useSessionUIStore } from "./session-ui-store"
import { useInputStore } from "./input-store"
import type { ChildStoreManager } from "./child-store"
import { opencodeClient } from "@/lib/opencode/client"
import { useGlobalSessionsStore } from "@/stores/useGlobalSessionsStore"
import { mergeSessionDirectoryMetadata, useGlobalSessionsStore } from "@/stores/useGlobalSessionsStore"
import { useConfigStore } from "@/stores/useConfigStore"
import { registerSessionDirectory } from "./sync-refs"
import { isSyntheticPart } from "@/lib/messages/synthetic"
import { materializeSessionSnapshots } from "./materialization"
import { stripMessageDiffSnapshots } from "./sanitize"
import { stripMessageDiffSnapshots, stripSessionDiffSnapshots } from "./sanitize"
import { sessionEvents } from "@/lib/sessionEvents"
import {
getOriginalSessionID,
@@ -127,6 +127,27 @@ function dirStoreForSession(sessionId: string): { store: DirectoryStoreApi; dire
return { store: dirStore(), directory: dir() }
}
function updateLiveSession(session: Session, directory?: string): void {
const stores = _childStores
if (!stores) return
const candidates = directory
? [[directory, stores.getChild(directory)] as const]
: stores.children
for (const [, store] of candidates) {
if (!store) continue
const current = store.getState().session
const index = current.findIndex((item) => item.id === session.id)
if (index === -1) continue
const next = [...current]
next[index] = mergeSessionDirectoryMetadata(session, current[index])
store.setState({ session: next })
return
}
}
function dir() {
return _getDirectory() || undefined
}
@@ -527,16 +548,18 @@ export async function updateSessionTitle(sessionId: string, title: string): Prom
export async function shareSession(sessionId: string): Promise<Session | null> {
const sessionDirectory = getSessionDirectory(sessionId)
const result = await sdk().session.share({ sessionID: sessionId, directory: sessionDirectory })
const session = assertSdkData(result, "session.share")
const session = stripSessionDiffSnapshots(assertSdkData(result, "session.share"))
useGlobalSessionsStore.getState().upsertSession(session)
updateLiveSession(session, sessionDirectory)
return session
}
export async function unshareSession(sessionId: string): Promise<Session | null> {
const sessionDirectory = getSessionDirectory(sessionId)
const result = await sdk().session.unshare({ sessionID: sessionId, directory: sessionDirectory })
const session = assertSdkData(result, "session.unshare")
const session = stripSessionDiffSnapshots(assertSdkData(result, "session.unshare"))
useGlobalSessionsStore.getState().upsertSession(session)
updateLiveSession(session, sessionDirectory)
return session
}