chore: generate

This commit is contained in:
opencode-agent[bot]
2026-06-25 02:18:06 +00:00
parent 3b4aaafd41
commit 5ca19947d9
7 changed files with 215 additions and 93 deletions
+11 -5
View File
@@ -52,7 +52,10 @@ export const createDirSyncContext = (
setStore("session", result.index, reconcile(session)) setStore("session", result.index, reconcile(session))
return return
} }
setStore("session", produce((draft) => void draft.splice(result.index, 0, session))) setStore(
"session",
produce((draft) => void draft.splice(result.index, 0, session)),
)
} }
return { return {
@@ -127,10 +130,13 @@ export const createDirSyncContext = (
more: createMemo(() => current()[0].session.length >= current()[0].limit), more: createMemo(() => current()[0].session.length >= current()[0].limit),
archive: async (sessionID: string) => { archive: async (sessionID: string) => {
await serverSDK.client.session.update({ sessionID, time: { archived: Date.now() } }) await serverSDK.client.session.update({ sessionID, time: { archived: Date.now() } })
current()[1]("session", produce((draft) => { current()[1](
const match = Binary.search(draft, sessionID, (session) => session.id) "session",
if (match.found) draft.splice(match.index, 1) produce((draft) => {
})) const match = Binary.search(draft, sessionID, (session) => session.id)
if (match.found) draft.splice(match.index, 1)
}),
)
}, },
}, },
mcp: { mcp: {
@@ -241,13 +241,18 @@ export async function bootstrapDirectory(input: {
input.sdk.session.status().then(async (x) => { input.sdk.session.status().then(async (x) => {
if (input.session) { if (input.session) {
const statuses = x.data ?? {} const statuses = x.data ?? {}
await Promise.all(Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined))) await Promise.all(
input.session.set("session_status", produce((draft) => { Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined)),
for (const sessionID of Object.keys(draft)) { )
if (statuses[sessionID]) continue input.session.set(
if (input.session?.get(sessionID)?.directory === input.directory) delete draft[sessionID] "session_status",
} produce((draft) => {
})) for (const sessionID of Object.keys(draft)) {
if (statuses[sessionID]) continue
if (input.session?.get(sessionID)?.directory === input.directory) delete draft[sessionID]
}
}),
)
for (const [sessionID, status] of Object.entries(statuses)) { for (const [sessionID, status] of Object.entries(statuses)) {
input.session.set("session_status", sessionID, reconcile(status)) input.session.set("session_status", sessionID, reconcile(status))
} }
@@ -69,12 +69,28 @@ describe("server session", () => {
ctx.store.pin("active") ctx.store.pin("active")
ctx.store.optimistic.add({ ctx.store.optimistic.add({
sessionID: "active", sessionID: "active",
message: { id: "message", sessionID: "active", role: "assistant", time: { created: 1 }, parentID: "parent", modelID: "model", providerID: "provider", mode: "build", agent: "agent", path: { cwd: "/repo", root: "/repo" }, cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } }, message: {
id: "message",
sessionID: "active",
role: "assistant",
time: { created: 1 },
parentID: "parent",
modelID: "model",
providerID: "provider",
mode: "build",
agent: "agent",
path: { cwd: "/repo", root: "/repo" },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
},
parts: [], parts: [],
}) })
for (let index = 0; index < 50; index++) { for (let index = 0; index < 50; index++) {
ctx.store.apply({ type: "session.status", properties: { sessionID: `session-${index}`, status: { type: "busy" } } }) ctx.store.apply({
type: "session.status",
properties: { sessionID: `session-${index}`, status: { type: "busy" } },
})
} }
expect(ctx.store.data.message.active?.map((message) => message.id)).toEqual(["message"]) expect(ctx.store.data.message.active?.map((message) => message.id)).toEqual(["message"])
+159 -71
View File
@@ -115,9 +115,15 @@ export function createServerSession(client: OpencodeClient) {
const preserve = new Set([ const preserve = new Set([
...pinned.keys(), ...pinned.keys(),
...requests.keys(), ...requests.keys(),
...Object.entries(data.permission).filter(([, items]) => items.length > 0).map(([sessionID]) => sessionID), ...Object.entries(data.permission)
...Object.entries(data.question).filter(([, items]) => items.length > 0).map(([sessionID]) => sessionID), .filter(([, items]) => items.length > 0)
...Object.entries(data.session_status).filter(([, status]) => status.type !== "idle").map(([sessionID]) => sessionID), .map(([sessionID]) => sessionID),
...Object.entries(data.question)
.filter(([, items]) => items.length > 0)
.map(([sessionID]) => sessionID),
...Object.entries(data.session_status)
.filter(([, status]) => status.type !== "idle")
.map(([sessionID]) => sessionID),
]) ])
for (const sessionID of preserve) { for (const sessionID of preserve) {
let current = data.info[sessionID] let current = data.info[sessionID]
@@ -132,7 +138,10 @@ export function createServerSession(client: OpencodeClient) {
if (!preserve.has(sessionID)) stale.push(sessionID) if (!preserve.has(sessionID)) stale.push(sessionID)
} }
stale.forEach((sessionID) => infoSeen.delete(sessionID)) stale.forEach((sessionID) => infoSeen.delete(sessionID))
setData("info", produce((draft) => stale.forEach((sessionID) => delete draft[sessionID]))) setData(
"info",
produce((draft) => stale.forEach((sessionID) => delete draft[sessionID])),
)
} }
return session return session
} }
@@ -214,27 +223,39 @@ export function createServerSession(client: OpencodeClient) {
) )
} }
const protectedSessions = () => new Set([ const protectedSessions = () =>
...pinned.keys(), new Set([
...requests.keys(), ...pinned.keys(),
...inflight.keys(), ...requests.keys(),
...inflightDiff.keys(), ...inflight.keys(),
...inflightTodo.keys(), ...inflightDiff.keys(),
...optimistic.keys(), ...inflightTodo.keys(),
...Object.entries(data.permission).filter(([, items]) => items.length > 0).map(([sessionID]) => sessionID), ...optimistic.keys(),
...Object.entries(data.question).filter(([, items]) => items.length > 0).map(([sessionID]) => sessionID), ...Object.entries(data.permission)
...Object.entries(data.session_status).filter(([, status]) => status.type !== "idle").map(([sessionID]) => sessionID), .filter(([, items]) => items.length > 0)
]) .map(([sessionID]) => sessionID),
...Object.entries(data.question)
.filter(([, items]) => items.length > 0)
.map(([sessionID]) => sessionID),
...Object.entries(data.session_status)
.filter(([, status]) => status.type !== "idle")
.map(([sessionID]) => sessionID),
])
const touch = (sessionID: string) => const touch = (sessionID: string) =>
evict(pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() })) evict(
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
)
const fetchMessages = async (sessionID: string, limit: number, before?: string) => { const fetchMessages = async (sessionID: string, limit: number, before?: string) => {
const response = await retry(() => client.session.messages({ sessionID, limit, before })) const response = await retry(() => client.session.messages({ sessionID, limit, before }))
const items = (response.data ?? []).filter((item) => !!item?.info?.id) const items = (response.data ?? []).filter((item) => !!item?.info?.id)
return { return {
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)), session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
part: items.map((item) => ({ id: item.info.id, part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) })), part: items.map((item) => ({
id: item.info.id,
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
})),
cursor: response.response.headers.get("x-next-cursor") ?? undefined, cursor: response.response.headers.get("x-next-cursor") ?? undefined,
complete: !response.response.headers.get("x-next-cursor"), complete: !response.response.headers.get("x-next-cursor"),
} }
@@ -284,7 +305,11 @@ export function createServerSession(client: OpencodeClient) {
const prefetch = async (sessionID: string, limit: number) => { const prefetch = async (sessionID: string, limit: number) => {
touch(sessionID) touch(sessionID)
await inflight.get(sessionID) await inflight.get(sessionID)
if (Date.now() - (meta.at[sessionID] ?? 0) <= 15_000 && (meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= limit)) return if (
Date.now() - (meta.at[sessionID] ?? 0) <= 15_000 &&
(meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= limit)
)
return
await runInflight(inflight, sessionID, () => loadMessages(sessionID, limit)) await runInflight(inflight, sessionID, () => loadMessages(sessionID, limit))
} }
@@ -292,8 +317,22 @@ export function createServerSession(client: OpencodeClient) {
const properties = event.properties const properties = event.properties
if (!properties || typeof properties !== "object") return if (!properties || typeof properties !== "object") return
if ("sessionID" in properties && typeof properties.sessionID === "string") return properties.sessionID if ("sessionID" in properties && typeof properties.sessionID === "string") return properties.sessionID
if ("info" in properties && properties.info && typeof properties.info === "object" && "sessionID" in properties.info && typeof properties.info.sessionID === "string") return properties.info.sessionID if (
if ("part" in properties && properties.part && typeof properties.part === "object" && "sessionID" in properties.part && typeof properties.part.sessionID === "string") return properties.part.sessionID "info" in properties &&
properties.info &&
typeof properties.info === "object" &&
"sessionID" in properties.info &&
typeof properties.info.sessionID === "string"
)
return properties.info.sessionID
if (
"part" in properties &&
properties.part &&
typeof properties.part === "object" &&
"sessionID" in properties.part &&
typeof properties.part.sessionID === "string"
)
return properties.part.sessionID
} }
const apply = (event: { type: string; properties?: unknown }) => { const apply = (event: { type: string; properties?: unknown }) => {
@@ -315,7 +354,10 @@ export function createServerSession(client: OpencodeClient) {
case "session.deleted": { case "session.deleted": {
const sessionID = (event.properties as { info: Session }).info.id const sessionID = (event.properties as { info: Session }).info.id
infoSeen.delete(sessionID) infoSeen.delete(sessionID)
setData("info", produce((draft) => void delete draft[sessionID])) setData(
"info",
produce((draft) => void delete draft[sessionID]),
)
evict([sessionID]) evict([sessionID])
return return
} }
@@ -343,30 +385,36 @@ export function createServerSession(client: OpencodeClient) {
} }
const result = Binary.search(messages, info.id, (message) => message.id) const result = Binary.search(messages, info.id, (message) => message.id)
if (result.found) setData("message", info.sessionID, result.index, reconcile(info)) if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
if (!result.found) setData("message", info.sessionID, (value = []) => { if (!result.found)
const next = value.slice() setData("message", info.sessionID, (value = []) => {
next.splice(result.index, 0, info) const next = value.slice()
return next next.splice(result.index, 0, info)
}) return next
})
return return
} }
case "message.removed": { case "message.removed": {
const props = event.properties as { sessionID: string; messageID: string } const props = event.properties as { sessionID: string; messageID: string }
setData(produce((draft) => { setData(
const messages = draft.message[props.sessionID] produce((draft) => {
if (messages) { const messages = draft.message[props.sessionID]
const result = Binary.search(messages, props.messageID, (message) => message.id) if (messages) {
if (result.found) messages.splice(result.index, 1) const result = Binary.search(messages, props.messageID, (message) => message.id)
} if (result.found) messages.splice(result.index, 1)
for (const part of draft.part[props.messageID] ?? []) delete draft.part_text_accum_delta[part.id] }
delete draft.part[props.messageID] for (const part of draft.part[props.messageID] ?? []) delete draft.part_text_accum_delta[part.id]
})) delete draft.part[props.messageID]
}),
)
return return
} }
case "message.part.updated": { case "message.part.updated": {
const part = (event.properties as { part: Part }).part const part = (event.properties as { part: Part }).part
if (SKIP_PARTS.has(part.type)) return if (SKIP_PARTS.has(part.type)) return
setData("part_text_accum_delta", produce((draft) => void delete draft[part.id])) setData(
"part_text_accum_delta",
produce((draft) => void delete draft[part.id]),
)
const parts = data.part[part.messageID] const parts = data.part[part.messageID]
if (!parts) { if (!parts) {
setData("part", part.messageID, [part]) setData("part", part.messageID, [part])
@@ -374,23 +422,26 @@ export function createServerSession(client: OpencodeClient) {
} }
const result = Binary.search(parts, part.id, (item) => item.id) const result = Binary.search(parts, part.id, (item) => item.id)
if (result.found) setData("part", part.messageID, result.index, reconcile(part)) if (result.found) setData("part", part.messageID, result.index, reconcile(part))
if (!result.found) setData("part", part.messageID, (value = []) => { if (!result.found)
const next = value.slice() setData("part", part.messageID, (value = []) => {
next.splice(result.index, 0, part) const next = value.slice()
return next next.splice(result.index, 0, part)
}) return next
})
return return
} }
case "message.part.removed": { case "message.part.removed": {
const props = event.properties as { messageID: string; partID: string } const props = event.properties as { messageID: string; partID: string }
setData(produce((draft) => { setData(
delete draft.part_text_accum_delta[props.partID] produce((draft) => {
const parts = draft.part[props.messageID] delete draft.part_text_accum_delta[props.partID]
if (!parts) return const parts = draft.part[props.messageID]
const result = Binary.search(parts, props.partID, (part) => part.id) if (!parts) return
if (result.found) parts.splice(result.index, 1) const result = Binary.search(parts, props.partID, (part) => part.id)
if (parts.length === 0) delete draft.part[props.messageID] if (result.found) parts.splice(result.index, 1)
})) if (parts.length === 0) delete draft.part[props.messageID]
}),
)
return return
} }
case "message.part.delta": { case "message.part.delta": {
@@ -401,13 +452,21 @@ export function createServerSession(client: OpencodeClient) {
if (!result.found) return if (!result.found) return
const field = props.field as keyof (typeof parts)[number] const field = props.field as keyof (typeof parts)[number]
const current = parts[result.index]?.[field] const current = parts[result.index]?.[field]
setData("part_text_accum_delta", props.partID, (value) => (value ?? (typeof current === "string" ? current : "")) + props.delta) setData(
setData("part", props.messageID, produce((draft) => { "part_text_accum_delta",
if (!draft) return props.partID,
const part = draft[result.index] (value) => (value ?? (typeof current === "string" ? current : "")) + props.delta,
const field = props.field as keyof typeof part )
;(part[field] as string) = ((part[field] as string | undefined) ?? "") + props.delta setData(
})) "part",
props.messageID,
produce((draft) => {
if (!draft) return
const part = draft[result.index]
const field = props.field as keyof typeof part
;(part[field] as string) = ((part[field] as string | undefined) ?? "") + props.delta
}),
)
return return
} }
case "permission.asked": { case "permission.asked": {
@@ -415,16 +474,25 @@ export function createServerSession(client: OpencodeClient) {
const permissions = data.permission[permission.sessionID] ?? [] const permissions = data.permission[permission.sessionID] ?? []
const result = Binary.search(permissions, permission.id, (item) => item.id) const result = Binary.search(permissions, permission.id, (item) => item.id)
if (result.found) setData("permission", permission.sessionID, result.index, reconcile(permission)) if (result.found) setData("permission", permission.sessionID, result.index, reconcile(permission))
if (!result.found) setData("permission", permission.sessionID, produce((draft = []) => void draft.splice(result.index, 0, permission))) if (!result.found)
setData(
"permission",
permission.sessionID,
produce((draft = []) => void draft.splice(result.index, 0, permission)),
)
return return
} }
case "permission.replied": { case "permission.replied": {
const props = event.properties as { sessionID: string; requestID: string } const props = event.properties as { sessionID: string; requestID: string }
setData("permission", props.sessionID, produce((draft) => { setData(
if (!draft) return "permission",
const result = Binary.search(draft, props.requestID, (item) => item.id) props.sessionID,
if (result.found) draft.splice(result.index, 1) produce((draft) => {
})) if (!draft) return
const result = Binary.search(draft, props.requestID, (item) => item.id)
if (result.found) draft.splice(result.index, 1)
}),
)
return return
} }
case "question.asked": { case "question.asked": {
@@ -432,17 +500,26 @@ export function createServerSession(client: OpencodeClient) {
const questions = data.question[question.sessionID] ?? [] const questions = data.question[question.sessionID] ?? []
const result = Binary.search(questions, question.id, (item) => item.id) const result = Binary.search(questions, question.id, (item) => item.id)
if (result.found) setData("question", question.sessionID, result.index, reconcile(question)) if (result.found) setData("question", question.sessionID, result.index, reconcile(question))
if (!result.found) setData("question", question.sessionID, produce((draft = []) => void draft.splice(result.index, 0, question))) if (!result.found)
setData(
"question",
question.sessionID,
produce((draft = []) => void draft.splice(result.index, 0, question)),
)
return return
} }
case "question.replied": case "question.replied":
case "question.rejected": { case "question.rejected": {
const props = event.properties as { sessionID: string; requestID: string } const props = event.properties as { sessionID: string; requestID: string }
setData("question", props.sessionID, produce((draft) => { setData(
if (!draft) return "question",
const result = Binary.search(draft, props.requestID, (item) => item.id) props.sessionID,
if (result.found) draft.splice(result.index, 1) produce((draft) => {
})) if (!draft) return
const result = Binary.search(draft, props.requestID, (item) => item.id)
if (result.found) draft.splice(result.index, 1)
}),
)
} }
} }
} }
@@ -478,12 +555,19 @@ export function createServerSession(client: OpencodeClient) {
if (items) items.set(input.message.id, input) if (items) items.set(input.message.id, input)
if (!items) optimistic.set(input.sessionID, new Map([[input.message.id, input]])) if (!items) optimistic.set(input.sessionID, new Map([[input.message.id, input]]))
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message])) setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
setData("part", input.message.id, input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))) setData(
"part",
input.message.id,
input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
)
}, },
remove(input: { sessionID: string; messageID: string }) { remove(input: { sessionID: string; messageID: string }) {
clearOptimistic(input.sessionID, input.messageID) clearOptimistic(input.sessionID, input.messageID)
setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID)) setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID))
setData("part", produce((draft) => void delete draft[input.messageID])) setData(
"part",
produce((draft) => void delete draft[input.messageID]),
)
}, },
}, },
diff(sessionID: string, options?: { force?: boolean }) { diff(sessionID: string, options?: { force?: boolean }) {
@@ -509,7 +593,11 @@ export function createServerSession(client: OpencodeClient) {
}) })
}, },
history: { history: {
more: (sessionID: string) => data.message[sessionID] !== undefined && meta.limit[sessionID] !== undefined && !meta.complete[sessionID] && !!meta.cursor[sessionID], more: (sessionID: string) =>
data.message[sessionID] !== undefined &&
meta.limit[sessionID] !== undefined &&
!meta.complete[sessionID] &&
!!meta.cursor[sessionID],
loading: (sessionID: string) => meta.loading[sessionID] ?? false, loading: (sessionID: string) => meta.loading[sessionID] ?? false,
async loadMore(sessionID: string, count = historyMessagePageSize) { async loadMore(sessionID: string, count = historyMessagePageSize) {
touch(sessionID) touch(sessionID)
+3 -1
View File
@@ -702,7 +702,9 @@ export default function LegacyLayout(props: ParentProps) {
} }
async function prefetchMessages(directory: string, sessionID: string, token: number) { async function prefetchMessages(directory: string, sessionID: string, token: number) {
await serverSync().session.prefetch(sessionID, prefetchChunk).catch(() => {}) await serverSync()
.session.prefetch(sessionID, prefetchChunk)
.catch(() => {})
if (prefetchToken.value !== token) return if (prefetchToken.value !== token) return
for (const stale of markPrefetched(directory, sessionID)) serverSync().session.evict(stale) for (const stale of markPrefetched(directory, sessionID)) serverSync().session.evict(stale)
} }
@@ -153,16 +153,23 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
const [sessionStore] = serverSync().child(props.session.directory) const [sessionStore] = serverSync().child(props.session.directory)
const hasPermissions = createMemo(() => { const hasPermissions = createMemo(() => {
return !!sessionPermissionRequest(sessionStore.session, serverSync().session.data.permission, props.session.id, (item) => { return !!sessionPermissionRequest(
return !permission.autoResponds(item, props.session.directory) sessionStore.session,
}) serverSync().session.data.permission,
props.session.id,
(item) => {
return !permission.autoResponds(item, props.session.directory)
},
)
}) })
const isWorking = createMemo(() => { const isWorking = createMemo(() => {
if (hasPermissions()) return false if (hasPermissions()) return false
return serverSync().session.data.session_working(props.session.id) return serverSync().session.data.session_working(props.session.id)
}) })
const tint = createMemo(() => messageAgentColor(serverSync().session.data.message[props.session.id], sessionStore.agent)) const tint = createMemo(() =>
messageAgentColor(serverSync().session.data.message[props.session.id], sessionStore.agent),
)
const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded())) const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded()))
const currentChild = createMemo(() => { const currentChild = createMemo(() => {
if (!props.showChild) return if (!props.showChild) return
+1 -3
View File
@@ -516,9 +516,7 @@ export default function Page() {
todoTimer = undefined todoTimer = undefined
if (!id) return if (!id) return
if (status === "idle" && !blocked) return if (status === "idle" && !blocked) return
const cached = untrack( const cached = untrack(() => sync().data.todo[id] !== undefined)
() => sync().data.todo[id] !== undefined,
)
todoFrame = requestAnimationFrame(() => { todoFrame = requestAnimationFrame(() => {
todoFrame = undefined todoFrame = undefined