Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 72 additions & 33 deletions packages/client/src/solid/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-

export type DataSessionStatus = "idle" | "running"
type OpenCodeEventMap = { [Type in OpenCodeEvent["type"]]: Extract<OpenCodeEvent, { type: Type }> }
type SessionPlacementEvent = OpenCodeEventMap["session.moved" | "worktree.resolved"]

export type CreateDataInput = {
readonly api: () => OpenCodeClient
Expand Down Expand Up @@ -213,6 +214,8 @@ export function createData(config: CreateDataInput) {
)
const messageIndex = new Map<string, Map<string, number>>()
const sync = createSync()
// Placement facts observed during a metadata read must compose in order, even for uncached family members.
const sessionPlacements = new Set<SessionPlacementEvent[]>()
let activeUpdates: Map<string, DataSessionStatus | undefined> | undefined

function setSessionActive(sessionID: string, status: DataSessionStatus) {
Expand Down Expand Up @@ -549,7 +552,20 @@ export function createData(config: CreateDataInput) {
)
}

function adoptionTarget(
info: Pick<SessionInfo, "projectID" | "location">,
event: OpenCodeEventMap["worktree.resolved"]["data"],
) {
const directory = event.adopted?.includes(info.projectID)
? store.project.info[info.projectID]?.canonical
: info.location.directory
if (!directory) return
return { projectID: info.projectID, directory, workspaceID: info.location.workspaceID }
}

function handleEvent(event: OpenCodeEvent) {
if (event.type === "session.moved" || event.type === "worktree.resolved")
sessionPlacements.forEach((events) => events.push(event))
switch (event.type) {
case "server.connected": {
const updates = new Map<string, DataSessionStatus | undefined>()
Expand Down Expand Up @@ -658,7 +674,7 @@ export function createData(config: CreateDataInput) {
projectID: current.projectID,
subpath: current.subpath,
}
setStore("session", "info", event.data.sessionID, "location", event.data.location)
setStore("session", "info", event.data.sessionID, "location", reconcile(event.data.location))
if (event.data.projectID) setStore("session", "info", event.data.sessionID, "projectID", event.data.projectID)
setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath)
message.insert(event.data.sessionID, {
Expand All @@ -675,22 +691,14 @@ export function createData(config: CreateDataInput) {
}
case "worktree.resolved": {
for (const [sessionID, info] of Object.entries(store.session.info)) {
const explicit = event.data.adopted?.includes(info.projectID)
const directory = explicit ? store.project.info[info.projectID]?.canonical : info.location.directory
if (!directory) {
const target = adoptionTarget(info, event.data)
if (!target) {
if (info.location.workspaceID) continue
result.session.invalidate(sessionID)
void result.session.sync(sessionID)
continue
}
const adopted = Worktree.adopt(
{
projectID: info.projectID,
directory,
workspaceID: info.location.workspaceID,
},
event.data,
)
const adopted = Worktree.adopt(target, event.data)
if (!adopted) continue
setStore("session", "info", sessionID, "projectID", adopted.projectID)
setStore("session", "info", sessionID, "subpath", adopted.subpath)
Expand Down Expand Up @@ -1481,29 +1489,60 @@ export function createData(config: CreateDataInput) {
})
},
sync(sessionID: string, options?: { children?: boolean }) {
return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, async () => {
const [info, children] = await Promise.all([
api().session.get({ sessionID }),
options?.children
? api()
.session.list({ parentID: sessionID, order: "desc" })
.then((response) => response.data)
: [],
])
const sessions = [info, ...children]
batch(() => {
setStore(
"session",
"info",
produce((draft) => {
for (const session of sessions) draft[session.id] = session
}),
return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, () => {
const placements: SessionPlacementEvent[] = []
sessionPlacements.add(placements)
const load = async (): Promise<void> => {
const [info, children] = await Promise.all([
api().session.get({ sessionID }),
options?.children
? api()
.session.list({ parentID: sessionID, order: "desc" })
.then((response) => response.data)
: [],
])
const sessions = [info, ...children].map((session) =>
placements.reduce<SessionInfo | undefined>((info, event) => {
// A complete move supersedes even an earlier adoption whose placement was unresolved.
if (event.type === "session.moved")
return event.data.sessionID === session.id
? {
...session,
location: { ...event.data.location },
projectID: event.data.projectID,
subpath: event.data.subpath,
}
: info
if (!info) return
const target = adoptionTarget(info, event.data)
if (!target && !info.location.workspaceID) return
const adopted = target && Worktree.adopt(target, event.data)
return adopted ? { ...info, ...adopted } : info
}, session),
)
for (const session of sessions) {
sync.complete(`session:${session.id}`)
registerSession(session.id)
const resolved = sessions.filter((session) => session !== undefined)
if (resolved.length !== sessions.length) {
// Explicit adoption needs the directory project's canonical path to derive subpath.
// Without it, read after the observed facts instead of guessing or publishing an old identity.
placements.length = 0
return load()
}
})
batch(() => {
setStore(
"session",
"info",
produce((draft) => {
// Publish once: even a transient old location can redirect the UI.
for (const session of resolved) draft[session.id] = session
}),
)
for (const session of resolved) {
sync.complete(`session:${session.id}`)
registerSession(session.id)
}
})
}
return load().finally(() => sessionPlacements.delete(placements))
})
},
invalidate(sessionID: string) {
Expand Down
Loading
Loading