Download packages/app/src/context/global-sync/event-reducer.ts from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/global-sync/event-reducer.ts
- Command line
-
hf download hf://SaylorTwift/opencode/packages/app/src/context/global-sync/event-reducer.ts
-
curl -L -o event-reducer.ts https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/global-sync/event-reducer.ts
16.7 kB
| import { Binary } from "@opencode-ai/core/util/binary" | |
| import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" | |
| import type { | |
| Message, | |
| Part, | |
| PermissionRequest, | |
| Project, | |
| QuestionRequest, | |
| Session, | |
| SessionStatus, | |
| Todo, | |
| } from "@opencode-ai/sdk/v2/client" | |
| import type { FileDiffInfo } from "@opencode-ai/client/promise" | |
| import type { State, VcsCache } from "./types" | |
| import { trimSessions } from "./session-trim" | |
| import { dropSessionCaches } from "./session-cache" | |
| import { diffs as list, message as clean } from "@/utils/diffs" | |
| import { messageKey } from "@/utils/session-message" | |
| const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) | |
| const SESSION_CONTENT_EVENTS = new Set([ | |
| "session.diff", | |
| "todo.updated", | |
| "session.status", | |
| "message.updated", | |
| "message.removed", | |
| "message.part.updated", | |
| "message.part.removed", | |
| "message.part.delta", | |
| "permission.asked", | |
| "permission.replied", | |
| "question.asked", | |
| "question.replied", | |
| "question.rejected", | |
| ]) | |
| export function applyGlobalEvent(input: { | |
| event: { type: string; properties?: unknown } | |
| project: Project[] | |
| setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void | |
| refresh: () => void | |
| }) { | |
| if (input.event.type === "global.disposed" || input.event.type === "server.connected") { | |
| input.refresh() | |
| return | |
| } | |
| if (input.event.type !== "project.updated") return | |
| const properties = input.event.properties as Project | |
| const result = Binary.search(input.project, properties.id, (s) => s.id) | |
| if (result.found) { | |
| input.setGlobalProject( | |
| produce((draft) => { | |
| draft[result.index] = { ...draft[result.index], ...properties } | |
| }), | |
| ) | |
| return | |
| } | |
| input.setGlobalProject( | |
| produce((draft) => { | |
| draft.splice(result.index, 0, properties) | |
| }), | |
| ) | |
| } | |
| function cleanupSessionCaches( | |
| setStore: SetStoreFunction<State>, | |
| sessionID: string, | |
| setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void, | |
| ) { | |
| if (!sessionID) return | |
| setSessionTodo?.(sessionID, undefined) | |
| setStore( | |
| produce((draft) => { | |
| dropSessionCaches(draft, [sessionID]) | |
| }), | |
| ) | |
| } | |
| export function cleanupDroppedSessionCaches( | |
| store: Store<State>, | |
| setStore: SetStoreFunction<State>, | |
| next: Session[], | |
| setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void, | |
| ) { | |
| const keep = new Set(next.map((item) => item.id)) | |
| const stale = [ | |
| ...Object.keys(store.message), | |
| ...Object.keys(store.session_diff), | |
| ...Object.keys(store.todo), | |
| ...Object.keys(store.permission), | |
| ...Object.keys(store.question), | |
| ...Object.keys(store.session_status), | |
| ...Object.values(store.part) | |
| .map((parts) => parts?.find((part) => !!part?.sessionID)?.sessionID) | |
| .filter((sessionID): sessionID is string => !!sessionID), | |
| ].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index) | |
| if (stale.length === 0) return | |
| for (const sessionID of stale) { | |
| setSessionTodo?.(sessionID, undefined) | |
| } | |
| setStore( | |
| produce((draft) => { | |
| dropSessionCaches(draft, stale) | |
| }), | |
| ) | |
| } | |
| export function applyDirectoryEvent(input: { | |
| event: { type: string; properties?: unknown } | |
| store: Store<State> | |
| setStore: SetStoreFunction<State> | |
| push: (directory: string) => void | |
| directory: string | |
| loadLsp: () => void | |
| loadReferences?: () => void | |
| vcsCache?: VcsCache | |
| setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void | |
| retainedLimit?: number | |
| sessionContent?: boolean | |
| permission?: State["permission"] | |
| }) { | |
| const event = input.event | |
| if (input.sessionContent === false && SESSION_CONTENT_EVENTS.has(event.type)) return | |
| const limit = Math.max(input.store.limit, input.retainedLimit ?? 0) | |
| switch (event.type) { | |
| case "server.instance.disposed": { | |
| input.push(input.directory) | |
| return | |
| } | |
| case "session.created": { | |
| const info = (event.properties as { info: Session }).info | |
| const result = Binary.search(input.store.session, info.id, (s) => s.id) | |
| if (result.found) { | |
| input.setStore("session", result.index, reconcile(info)) | |
| break | |
| } | |
| const next = input.store.session.slice() | |
| next.splice(result.index, 0, info) | |
| const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission }) | |
| input.setStore("session", reconcile(trimmed, { key: "id" })) | |
| cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) | |
| if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1) | |
| break | |
| } | |
| case "session.updated": { | |
| const info = (event.properties as { info: Session }).info | |
| const result = Binary.search(input.store.session, info.id, (s) => s.id) | |
| if (info.time.archived) { | |
| if (!result.found) break | |
| if (input.store.session[result.index]!.time.archived === info.time.archived) break | |
| input.setStore( | |
| "session", | |
| produce((draft) => { | |
| draft.splice(result.index, 1) | |
| }), | |
| ) | |
| cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo) | |
| if (info.parentID) break | |
| input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) | |
| break | |
| } | |
| if (result.found) { | |
| input.setStore("session", result.index, reconcile(info)) | |
| break | |
| } | |
| const next = input.store.session.slice() | |
| next.splice(result.index, 0, info) | |
| const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission }) | |
| input.setStore("session", reconcile(trimmed, { key: "id" })) | |
| cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) | |
| break | |
| } | |
| case "session.deleted": { | |
| const properties = event.properties as { sessionID?: string; info?: Session } | |
| const sessionID = properties.info?.id ?? properties.sessionID | |
| if (!sessionID) break | |
| const result = Binary.search(input.store.session, sessionID, (s) => s.id) | |
| const info = properties.info ?? (result.found ? input.store.session[result.index] : undefined) | |
| if (result.found) { | |
| input.setStore( | |
| "session", | |
| produce((draft) => { | |
| draft.splice(result.index, 1) | |
| }), | |
| ) | |
| } | |
| cleanupSessionCaches(input.setStore, sessionID, input.setSessionTodo) | |
| if (info?.parentID) break | |
| input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) | |
| break | |
| } | |
| case "session.renamed": { | |
| const properties = event.properties as { sessionID: string; title: string } | |
| const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) | |
| if (!result.found) break | |
| input.setStore("session", result.index, (session) => ({ | |
| ...session, | |
| title: properties.title, | |
| time: { ...session.time, updated: Date.now() }, | |
| })) | |
| break | |
| } | |
| case "session.usage.updated": { | |
| const properties = event.properties as Pick<Session, "cost" | "tokens"> & { sessionID: string } | |
| const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) | |
| if (!result.found) break | |
| input.setStore("session", result.index, (session) => ({ | |
| ...session, | |
| cost: properties.cost, | |
| tokens: properties.tokens, | |
| })) | |
| break | |
| } | |
| // case "session.archived": { | |
| // const properties = event.properties as { sessionID: string } | |
| // const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) | |
| // if (!result.found) break | |
| // const info = input.store.session[result.index] | |
| // input.setStore( | |
| // "session", | |
| // produce((draft) => void draft.splice(result.index, 1)), | |
| // ) | |
| // cleanupSessionCaches(input.setStore, properties.sessionID) | |
| // if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) | |
| // break | |
| // } | |
| case "session.moved": { | |
| const properties = event.properties as { | |
| sessionID: string | |
| location: { directory: string; workspaceID?: string } | |
| projectID?: string | |
| subpath?: string | |
| } | |
| const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) | |
| if (!result.found) break | |
| if (properties.location.directory === input.directory) { | |
| input.setStore("session", result.index, (session) => ({ | |
| ...session, | |
| projectID: properties.projectID ?? session.projectID, | |
| workspaceID: properties.location.workspaceID, | |
| directory: properties.location.directory, | |
| path: properties.subpath, | |
| time: { ...session.time, updated: Date.now() }, | |
| })) | |
| break | |
| } | |
| const info = input.store.session[result.index] | |
| input.setStore( | |
| "session", | |
| produce((draft) => void draft.splice(result.index, 1)), | |
| ) | |
| if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) | |
| break | |
| } | |
| case "session.diff": { | |
| const props = event.properties as { sessionID: string; diff: FileDiffInfo[] } | |
| input.setStore("session_diff", props.sessionID, reconcile(list(props.diff) as FileDiffInfo[], { key: "file" })) | |
| break | |
| } | |
| case "todo.updated": { | |
| const props = event.properties as { sessionID: string; todos: Todo[] } | |
| input.setStore("todo", props.sessionID, reconcile(props.todos, { key: "id" })) | |
| input.setSessionTodo?.(props.sessionID, props.todos) | |
| break | |
| } | |
| case "session.status": { | |
| const props = event.properties as { sessionID: string; status: SessionStatus } | |
| input.setStore("session_status", props.sessionID, reconcile(props.status)) | |
| break | |
| } | |
| case "message.updated": { | |
| const info = clean((event.properties as { info: Message }).info) | |
| const messages = input.store.message[info.sessionID] | |
| if (!messages) { | |
| input.setStore("message", info.sessionID, [info]) | |
| break | |
| } | |
| const result = Binary.search(messages, messageKey(info), messageKey) | |
| if (result.found) { | |
| input.setStore("message", info.sessionID, result.index, reconcile(info)) | |
| break | |
| } | |
| input.setStore( | |
| "message", | |
| info.sessionID, | |
| produce((draft) => { | |
| draft.splice(result.index, 0, info) | |
| }), | |
| ) | |
| break | |
| } | |
| case "message.removed": { | |
| const props = event.properties as { sessionID: string; messageID: string } | |
| input.setStore( | |
| produce((draft) => { | |
| const messages = draft.message[props.sessionID] | |
| if (messages) { | |
| const index = messages.findIndex((message) => message.id === props.messageID) | |
| if (index >= 0) messages.splice(index, 1) | |
| } | |
| const parts = draft.part[props.messageID] | |
| if (parts) { | |
| for (const part of parts) { | |
| delete draft.part_text_accum_delta[part.id] | |
| } | |
| } | |
| delete draft.part[props.messageID] | |
| }), | |
| ) | |
| break | |
| } | |
| case "message.part.updated": { | |
| const part = (event.properties as { part: Part }).part | |
| if (SKIP_PARTS.has(part.type)) break | |
| input.setStore( | |
| produce((draft) => { | |
| delete draft.part_text_accum_delta[part.id] | |
| }), | |
| ) | |
| const parts = input.store.part[part.messageID] | |
| if (!parts) { | |
| input.setStore("part", part.messageID, [part]) | |
| break | |
| } | |
| const result = Binary.search(parts, part.id, (item) => item.id) | |
| if (result.found) { | |
| input.setStore("part", part.messageID, result.index, reconcile(part)) | |
| break | |
| } | |
| input.setStore( | |
| "part", | |
| part.messageID, | |
| produce((draft) => { | |
| draft.splice(result.index, 0, part) | |
| }), | |
| ) | |
| break | |
| } | |
| case "message.part.removed": { | |
| const props = event.properties as { messageID: string; partID: string } | |
| input.setStore( | |
| produce((draft) => { | |
| delete draft.part_text_accum_delta[props.partID] | |
| }), | |
| ) | |
| const parts = input.store.part[props.messageID] | |
| if (!parts) break | |
| const result = Binary.search(parts, props.partID, (part) => part.id) | |
| if (result.found) { | |
| input.setStore( | |
| produce((draft) => { | |
| const list = draft.part[props.messageID] | |
| if (!list) return | |
| const next = Binary.search(list, props.partID, (part) => part.id) | |
| if (!next.found) return | |
| list.splice(next.index, 1) | |
| if (list.length === 0) delete draft.part[props.messageID] | |
| }), | |
| ) | |
| } | |
| break | |
| } | |
| case "message.part.delta": { | |
| const props = event.properties as { messageID: string; partID: string; field: string; delta: string } | |
| const parts = input.store.part[props.messageID] | |
| if (!parts) break | |
| const result = Binary.search(parts, props.partID, (part) => part.id) | |
| if (!result.found) break | |
| const field = props.field as keyof (typeof parts)[number] | |
| const current = parts[result.index]?.[field] | |
| input.setStore( | |
| "part_text_accum_delta", | |
| props.partID, | |
| (existing) => (existing ?? (typeof current === "string" ? current : "")) + props.delta, | |
| ) | |
| input.setStore( | |
| "part", | |
| props.messageID, | |
| produce((draft) => { | |
| const part = draft[result.index] | |
| const field = props.field as keyof typeof part | |
| const existing = part[field] as string | undefined | |
| ;(part[field] as string) = (existing ?? "") + props.delta | |
| }), | |
| ) | |
| break | |
| } | |
| case "vcs.branch.updated": { | |
| const props = event.properties as { branch?: string } | |
| if (input.store.vcs?.branch === props.branch) break | |
| const next = { ...input.store.vcs, branch: props.branch } | |
| input.setStore("vcs", next) | |
| if (input.vcsCache) input.vcsCache.setStore("value", next) | |
| break | |
| } | |
| case "permission.asked": { | |
| const permission = event.properties as PermissionRequest | |
| const permissions = input.store.permission[permission.sessionID] | |
| if (!permissions) { | |
| input.setStore("permission", permission.sessionID, [permission]) | |
| break | |
| } | |
| const result = Binary.search(permissions, permission.id, (p) => p.id) | |
| if (result.found) { | |
| input.setStore("permission", permission.sessionID, result.index, reconcile(permission)) | |
| break | |
| } | |
| input.setStore( | |
| "permission", | |
| permission.sessionID, | |
| produce((draft) => { | |
| draft.splice(result.index, 0, permission) | |
| }), | |
| ) | |
| break | |
| } | |
| case "permission.replied": { | |
| const props = event.properties as { sessionID: string; requestID: string } | |
| const permissions = input.store.permission[props.sessionID] | |
| if (!permissions) break | |
| const result = Binary.search(permissions, props.requestID, (p) => p.id) | |
| if (!result.found) break | |
| input.setStore( | |
| "permission", | |
| props.sessionID, | |
| produce((draft) => { | |
| draft.splice(result.index, 1) | |
| }), | |
| ) | |
| break | |
| } | |
| case "question.asked": { | |
| const question = event.properties as QuestionRequest | |
| const questions = input.store.question[question.sessionID] | |
| if (!questions) { | |
| input.setStore("question", question.sessionID, [question]) | |
| break | |
| } | |
| const result = Binary.search(questions, question.id, (q) => q.id) | |
| if (result.found) { | |
| input.setStore("question", question.sessionID, result.index, reconcile(question)) | |
| break | |
| } | |
| input.setStore( | |
| "question", | |
| question.sessionID, | |
| produce((draft) => { | |
| draft.splice(result.index, 0, question) | |
| }), | |
| ) | |
| break | |
| } | |
| case "question.replied": | |
| case "question.rejected": { | |
| const props = event.properties as { sessionID: string; requestID: string } | |
| const questions = input.store.question[props.sessionID] | |
| if (!questions) break | |
| const result = Binary.search(questions, props.requestID, (q) => q.id) | |
| if (!result.found) break | |
| input.setStore( | |
| "question", | |
| props.sessionID, | |
| produce((draft) => { | |
| draft.splice(result.index, 1) | |
| }), | |
| ) | |
| break | |
| } | |
| case "lsp.updated": { | |
| input.loadLsp() | |
| break | |
| } | |
| case "reference.updated": { | |
| input.loadReferences?.() | |
| break | |
| } | |
| } | |
| } | |