Skip to content

Commit 2b27b32

Browse files
committed
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
1 parent 5906d89 commit 2b27b32

2 files changed

Lines changed: 159 additions & 5 deletions

File tree

packages/ui/src/sync/session-actions.test.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const scopedClientDirectories: string[] = []
88
const registeredSessionDirectories: Array<{ sessionID: string; directory: string }> = []
99
let sessionRevertResult: { data?: unknown; error?: unknown; response?: { status?: number } } = {}
1010
let questionReplyError: unknown | null = null
11+
let sessionShareResult: { data?: unknown; error?: unknown; response?: { status?: number } } = {}
12+
const globalUpsertedSessions: unknown[] = []
1113

1214
const mockScopedClient = {
1315
permission: {
@@ -45,6 +47,14 @@ const mockSdk = {
4547
replyCalls.push({ method: "session.abort", params })
4648
return Promise.resolve({ data: true })
4749
}),
50+
share: mock((params: Record<string, unknown>) => {
51+
replyCalls.push({ method: "session.share", params })
52+
return Promise.resolve(sessionShareResult)
53+
}),
54+
unshare: mock((params: Record<string, unknown>) => {
55+
replyCalls.push({ method: "session.unshare", params })
56+
return Promise.resolve(sessionShareResult)
57+
}),
4858
},
4959
permission: {
5060
reply: mock((params: Record<string, unknown>) => {
@@ -141,9 +151,21 @@ mock.module("./input-store", () => ({
141151
}))
142152

143153
mock.module("@/stores/useGlobalSessionsStore", () => ({
154+
mergeSessionDirectoryMetadata: (incoming: Session, existing?: SessionWithDirectory | null): SessionWithDirectory => {
155+
if (!existing) return incoming as SessionWithDirectory
156+
const next = { ...(incoming as SessionWithDirectory) }
157+
if (!next.directory && existing.directory) next.directory = existing.directory
158+
if (!next.project && existing.project) next.project = existing.project
159+
if (next.project && !next.project.worktree && existing.project?.worktree) {
160+
next.project = { ...next.project, worktree: existing.project.worktree }
161+
}
162+
return next
163+
},
144164
useGlobalSessionsStore: {
145165
getState: () => ({
146-
upsertSession: () => {},
166+
upsertSession: (session: unknown) => {
167+
globalUpsertedSessions.push(session)
168+
},
147169
}),
148170
},
149171
}))
@@ -161,6 +183,10 @@ import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2
161183

162184
type OptimisticAddCall = { sessionID: string; directory?: string | null; message: Message; parts: Part[] }
163185
type OptimisticRemoveCall = { sessionID: string; directory?: string | null; messageID: string }
186+
type SessionWithDirectory = Session & {
187+
directory?: string | null
188+
project?: { worktree?: string | null }
189+
}
164190

165191
function createStore(
166192
permissions: Record<string, PermissionRequest[]>,
@@ -183,9 +209,114 @@ function createChildStores(entries: Array<[string, StoreApi<DirectoryStore>]>) {
183209
if (!store) throw new Error(`No store for ${dir}`)
184210
return store
185211
},
212+
getChild: (dir: string) => new Map(entries).get(dir),
186213
} as unknown as import("./child-store").ChildStoreManager
187214
}
188215

216+
describe("shareSession live state", () => {
217+
beforeEach(() => {
218+
replyCalls.length = 0
219+
globalUpsertedSessions.length = 0
220+
sessionShareResult = {}
221+
})
222+
223+
test("updates the directory live store after unsharing", async () => {
224+
const sharedSession = { id: "session-a", time: { created: 1 }, share: { url: "https://share.example/a" } } as Session
225+
const unsharedSession = { id: "session-a", time: { created: 1, updated: 2 } } as Session
226+
const sessionStore = createStore({}, { session: [sharedSession] })
227+
const otherStore = createStore({}, { session: [{ id: "other", time: { created: 1 } } as Session] })
228+
const childStores = createChildStores([
229+
["/test/project", sessionStore],
230+
["/other/project", otherStore],
231+
])
232+
sessionShareResult = { data: unsharedSession }
233+
234+
const { setActionRefs, unshareSession } = await import("./session-actions")
235+
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
236+
237+
const result = await unshareSession("session-a")
238+
239+
expect(result).toBe(unsharedSession)
240+
expect(replyCalls.find((call) => call.method === "session.unshare")?.params.directory).toBe("/test/project")
241+
expect(sessionStore.getState().session[0].share).toBe(undefined)
242+
expect(otherStore.getState().session[0].id).toBe("other")
243+
expect(globalUpsertedSessions).toEqual([unsharedSession])
244+
})
245+
246+
test("updates the directory live store after sharing", async () => {
247+
const unsharedSession = { id: "session-a", time: { created: 1 } } as Session
248+
const sharedSession = { id: "session-a", time: { created: 1, updated: 2 }, share: { url: "https://share.example/a" } } as Session
249+
const sessionStore = createStore({}, { session: [unsharedSession] })
250+
const childStores = createChildStores([["/test/project", sessionStore]])
251+
sessionShareResult = { data: sharedSession }
252+
253+
const { setActionRefs, shareSession } = await import("./session-actions")
254+
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
255+
256+
const result = await shareSession("session-a")
257+
258+
expect(result).toBe(sharedSession)
259+
expect(replyCalls.find((call) => call.method === "session.share")?.params.directory).toBe("/test/project")
260+
expect(sessionStore.getState().session[0].share?.url).toBe("https://share.example/a")
261+
expect(globalUpsertedSessions).toEqual([sharedSession])
262+
})
263+
264+
test("preserves live directory metadata while clearing share from null response", async () => {
265+
const sharedSession = {
266+
id: "session-a",
267+
time: { created: 1 },
268+
directory: "/test/project",
269+
project: { worktree: "/test/project" },
270+
share: { url: "https://share.example/a" },
271+
} as SessionWithDirectory
272+
const unsharedSession = {
273+
id: "session-a",
274+
time: { created: 1, updated: 2 },
275+
share: null,
276+
} as unknown as Session
277+
const sessionStore = createStore({}, { session: [sharedSession] })
278+
const childStores = createChildStores([["/test/project", sessionStore]])
279+
sessionShareResult = { data: unsharedSession }
280+
281+
const { setActionRefs, unshareSession } = await import("./session-actions")
282+
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
283+
284+
await unshareSession("session-a")
285+
286+
const liveSession = sessionStore.getState().session[0] as SessionWithDirectory & { share?: null }
287+
expect(liveSession.share).toBe(null)
288+
expect(liveSession.directory).toBe("/test/project")
289+
expect(liveSession.project?.worktree).toBe("/test/project")
290+
})
291+
292+
test("strips oversized diff snapshots before updating session stores", async () => {
293+
const sessionWithDiff = {
294+
id: "session-a",
295+
time: { created: 1, updated: 2 },
296+
share: { url: "https://share.example/a" },
297+
summary: {
298+
diffs: [{ file: "a.txt", before: "old", after: "new", additions: 1, deletions: 1 }],
299+
},
300+
} as unknown as Session
301+
const sessionStore = createStore({}, { session: [{ id: "session-a", time: { created: 1 } } as Session] })
302+
const childStores = createChildStores([["/test/project", sessionStore]])
303+
sessionShareResult = { data: sessionWithDiff }
304+
305+
const { setActionRefs, shareSession } = await import("./session-actions")
306+
setActionRefs(mockSdk as unknown as OpencodeClient, childStores, () => "/current/project")
307+
308+
const result = await shareSession("session-a")
309+
310+
const storedDiff = ((sessionStore.getState().session[0] as { summary?: { diffs?: Array<Record<string, unknown>> } }).summary?.diffs ?? [])[0]
311+
const globalDiff = (((globalUpsertedSessions[0] as { summary?: { diffs?: Array<Record<string, unknown>> } }).summary?.diffs ?? [])[0])
312+
const resultDiff = ((result as { summary?: { diffs?: Array<Record<string, unknown>> } }).summary?.diffs ?? [])[0]
313+
expect(storedDiff.before).toBe(undefined)
314+
expect(storedDiff.after).toBe(undefined)
315+
expect(globalDiff.before).toBe(undefined)
316+
expect(resultDiff.after).toBe(undefined)
317+
})
318+
})
319+
189320
describe("optimisticSend target directory", () => {
190321
beforeEach(() => {
191322
replyCalls.length = 0

packages/ui/src/sync/session-actions.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import { useSessionUIStore } from "./session-ui-store"
99
import { useInputStore } from "./input-store"
1010
import type { ChildStoreManager } from "./child-store"
1111
import { opencodeClient } from "@/lib/opencode/client"
12-
import { useGlobalSessionsStore } from "@/stores/useGlobalSessionsStore"
12+
import { mergeSessionDirectoryMetadata, useGlobalSessionsStore } from "@/stores/useGlobalSessionsStore"
1313
import { useConfigStore } from "@/stores/useConfigStore"
1414
import { registerSessionDirectory } from "./sync-refs"
1515
import { isSyntheticPart } from "@/lib/messages/synthetic"
1616
import { materializeSessionSnapshots } from "./materialization"
17-
import { stripMessageDiffSnapshots } from "./sanitize"
17+
import { stripMessageDiffSnapshots, stripSessionDiffSnapshots } from "./sanitize"
1818
import { sessionEvents } from "@/lib/sessionEvents"
1919
import {
2020
getOriginalSessionID,
@@ -127,6 +127,27 @@ function dirStoreForSession(sessionId: string): { store: DirectoryStoreApi; dire
127127
return { store: dirStore(), directory: dir() }
128128
}
129129

130+
function updateLiveSession(session: Session, directory?: string): void {
131+
const stores = _childStores
132+
if (!stores) return
133+
134+
const candidates = directory
135+
? [[directory, stores.getChild(directory)] as const]
136+
: stores.children
137+
138+
for (const [, store] of candidates) {
139+
if (!store) continue
140+
const current = store.getState().session
141+
const index = current.findIndex((item) => item.id === session.id)
142+
if (index === -1) continue
143+
144+
const next = [...current]
145+
next[index] = mergeSessionDirectoryMetadata(session, current[index])
146+
store.setState({ session: next })
147+
return
148+
}
149+
}
150+
130151
function dir() {
131152
return _getDirectory() || undefined
132153
}
@@ -527,16 +548,18 @@ export async function updateSessionTitle(sessionId: string, title: string): Prom
527548
export async function shareSession(sessionId: string): Promise<Session | null> {
528549
const sessionDirectory = getSessionDirectory(sessionId)
529550
const result = await sdk().session.share({ sessionID: sessionId, directory: sessionDirectory })
530-
const session = assertSdkData(result, "session.share")
551+
const session = stripSessionDiffSnapshots(assertSdkData(result, "session.share"))
531552
useGlobalSessionsStore.getState().upsertSession(session)
553+
updateLiveSession(session, sessionDirectory)
532554
return session
533555
}
534556

535557
export async function unshareSession(sessionId: string): Promise<Session | null> {
536558
const sessionDirectory = getSessionDirectory(sessionId)
537559
const result = await sdk().session.unshare({ sessionID: sessionId, directory: sessionDirectory })
538-
const session = assertSdkData(result, "session.unshare")
560+
const session = stripSessionDiffSnapshots(assertSdkData(result, "session.unshare"))
539561
useGlobalSessionsStore.getState().upsertSession(session)
562+
updateLiveSession(session, sessionDirectory)
540563
return session
541564
}
542565

0 commit comments

Comments
 (0)