diff options
| author | Adam <[email protected]> | 2026-02-12 11:26:19 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-12 11:26:19 -0600 |
| commit | da952135cabba2926698298797cd301e7adaf48c (patch) | |
| tree | 78635fe4f7d656266ad3cc1c353b04b56969515c /packages/app/src/context | |
| parent | 789705ea96ae28af7e30801fd6039ce89b6ac48e (diff) | |
| download | opencode-da952135cabba2926698298797cd301e7adaf48c.tar.gz opencode-da952135cabba2926698298797cd301e7adaf48c.zip | |
chore(app): refactor for better solidjs hygiene (#13344)
Diffstat (limited to 'packages/app/src/context')
| -rw-r--r-- | packages/app/src/context/file/view-cache.ts | 52 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/event-reducer.ts | 1 | ||||
| -rw-r--r-- | packages/app/src/context/notification.tsx | 185 | ||||
| -rw-r--r-- | packages/app/src/context/terminal.tsx | 49 |
4 files changed, 230 insertions, 57 deletions
diff --git a/packages/app/src/context/file/view-cache.ts b/packages/app/src/context/file/view-cache.ts index 2614b2fb5..6e8ddf62d 100644 --- a/packages/app/src/context/file/view-cache.ts +++ b/packages/app/src/context/file/view-cache.ts @@ -23,6 +23,16 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange { } } +function equalSelectedLines(a: SelectedLineRange | null | undefined, b: SelectedLineRange | null | undefined) { + if (!a && !b) return true + if (!a || !b) return false + const left = normalizeSelectedLines(a) + const right = normalizeSelectedLines(b) + return ( + left.start === right.start && left.end === right.end && left.side === right.side && left.endSide === right.endSide + ) +} + function createViewSession(dir: string, id: string | undefined) { const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1` @@ -65,36 +75,36 @@ function createViewSession(dir: string, id: string | undefined) { const selectedLines = (path: string) => view.file[path]?.selectedLines const setScrollTop = (path: string, top: number) => { - setView("file", path, (current) => { - if (current?.scrollTop === top) return current - return { - ...(current ?? {}), - scrollTop: top, - } - }) + setView( + produce((draft) => { + const file = draft.file[path] ?? (draft.file[path] = {}) + if (file.scrollTop === top) return + file.scrollTop = top + }), + ) pruneView(path) } const setScrollLeft = (path: string, left: number) => { - setView("file", path, (current) => { - if (current?.scrollLeft === left) return current - return { - ...(current ?? {}), - scrollLeft: left, - } - }) + setView( + produce((draft) => { + const file = draft.file[path] ?? (draft.file[path] = {}) + if (file.scrollLeft === left) return + file.scrollLeft = left + }), + ) pruneView(path) } const setSelectedLines = (path: string, range: SelectedLineRange | null) => { const next = range ? normalizeSelectedLines(range) : null - setView("file", path, (current) => { - if (current?.selectedLines === next) return current - return { - ...(current ?? {}), - selectedLines: next, - } - }) + setView( + produce((draft) => { + const file = draft.file[path] ?? (draft.file[path] = {}) + if (equalSelectedLines(file.selectedLines, next)) return + file.selectedLines = next + }), + ) pruneView(path) } diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index c658d82c8..fa1a43d47 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -233,6 +233,7 @@ export function applyDirectoryEvent(input: { } case "vcs.branch.updated": { const props = event.properties as { branch: string } + if (input.store.vcs?.branch === props.branch) break const next = { branch: props.branch } input.setStore("vcs", next) if (input.vcsCache) input.vcsCache.setStore("value", next) diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index e35e815f9..bf880d115 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,5 +1,5 @@ -import { createStore } from "solid-js/store" -import { createEffect, createMemo, onCleanup } from "solid-js" +import { createStore, reconcile } from "solid-js/store" +import { batch, createEffect, createMemo, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" @@ -13,7 +13,6 @@ import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" import { playSound, soundSrc } from "@/utils/sound" -import { buildNotificationIndex } from "./notification-index" type NotificationBase = { directory?: string @@ -34,6 +33,21 @@ type ErrorNotification = NotificationBase & { export type Notification = TurnCompleteNotification | ErrorNotification +type NotificationIndex = { + session: { + all: Record<string, Notification[]> + unseen: Record<string, Notification[]> + unseenCount: Record<string, number> + unseenHasError: Record<string, boolean> + } + project: { + all: Record<string, Notification[]> + unseen: Record<string, Notification[]> + unseenCount: Record<string, number> + unseenHasError: Record<string, boolean> + } +} + const MAX_NOTIFICATIONS = 500 const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30 @@ -44,6 +58,53 @@ function pruneNotifications(list: Notification[]) { return pruned.slice(pruned.length - MAX_NOTIFICATIONS) } +function createNotificationIndex(): NotificationIndex { + return { + session: { + all: {}, + unseen: {}, + unseenCount: {}, + unseenHasError: {}, + }, + project: { + all: {}, + unseen: {}, + unseenCount: {}, + unseenHasError: {}, + }, + } +} + +function buildNotificationIndex(list: Notification[]) { + const index = createNotificationIndex() + + list.forEach((notification) => { + if (notification.session) { + const all = index.session.all[notification.session] ?? [] + index.session.all[notification.session] = [...all, notification] + if (!notification.viewed) { + const unseen = index.session.unseen[notification.session] ?? [] + index.session.unseen[notification.session] = [...unseen, notification] + index.session.unseenCount[notification.session] = unseen.length + 1 + if (notification.type === "error") index.session.unseenHasError[notification.session] = true + } + } + + if (notification.directory) { + const all = index.project.all[notification.directory] ?? [] + index.project.all[notification.directory] = [...all, notification] + if (!notification.viewed) { + const unseen = index.project.unseen[notification.directory] ?? [] + index.project.unseen[notification.directory] = [...unseen, notification] + index.project.unseenCount[notification.directory] = unseen.length + 1 + if (notification.type === "error") index.project.unseenHasError[notification.directory] = true + } + } + }) + + return index +} + export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ name: "Notification", init: () => { @@ -68,21 +129,81 @@ export const { use: useNotification, provider: NotificationProvider } = createSi list: [] as Notification[], }), ) + const [index, setIndex] = createStore<NotificationIndex>(buildNotificationIndex(store.list)) const meta = { pruned: false, disposed: false } + const updateUnseen = (scope: "session" | "project", key: string, unseen: Notification[]) => { + setIndex(scope, "unseen", key, unseen) + setIndex(scope, "unseenCount", key, unseen.length) + setIndex( + scope, + "unseenHasError", + key, + unseen.some((notification) => notification.type === "error"), + ) + } + + const appendToIndex = (notification: Notification) => { + if (notification.session) { + setIndex("session", "all", notification.session, (all = []) => [...all, notification]) + if (!notification.viewed) { + setIndex("session", "unseen", notification.session, (unseen = []) => [...unseen, notification]) + setIndex("session", "unseenCount", notification.session, (count = 0) => count + 1) + if (notification.type === "error") setIndex("session", "unseenHasError", notification.session, true) + } + } + + if (notification.directory) { + setIndex("project", "all", notification.directory, (all = []) => [...all, notification]) + if (!notification.viewed) { + setIndex("project", "unseen", notification.directory, (unseen = []) => [...unseen, notification]) + setIndex("project", "unseenCount", notification.directory, (count = 0) => count + 1) + if (notification.type === "error") setIndex("project", "unseenHasError", notification.directory, true) + } + } + } + + const removeFromIndex = (notification: Notification) => { + if (notification.session) { + setIndex("session", "all", notification.session, (all = []) => all.filter((n) => n !== notification)) + if (!notification.viewed) { + const unseen = (index.session.unseen[notification.session] ?? empty).filter((n) => n !== notification) + updateUnseen("session", notification.session, unseen) + } + } + + if (notification.directory) { + setIndex("project", "all", notification.directory, (all = []) => all.filter((n) => n !== notification)) + if (!notification.viewed) { + const unseen = (index.project.unseen[notification.directory] ?? empty).filter((n) => n !== notification) + updateUnseen("project", notification.directory, unseen) + } + } + } + createEffect(() => { if (!ready()) return if (meta.pruned) return meta.pruned = true - setStore("list", pruneNotifications(store.list)) + const list = pruneNotifications(store.list) + batch(() => { + setStore("list", list) + setIndex(reconcile(buildNotificationIndex(list), { merge: false })) + }) }) const append = (notification: Notification) => { - setStore("list", (list) => pruneNotifications([...list, notification])) - } + const list = pruneNotifications([...store.list, notification]) + const keep = new Set(list) + const removed = store.list.filter((n) => !keep.has(n)) - const index = createMemo(() => buildNotificationIndex(store.list)) + batch(() => { + if (keep.has(notification)) appendToIndex(notification) + removed.forEach((n) => removeFromIndex(n)) + setStore("list", list) + }) + } const lookup = async (directory: string, sessionID?: string) => { if (!sessionID) return undefined @@ -181,36 +302,66 @@ export const { use: useNotification, provider: NotificationProvider } = createSi ready, session: { all(session: string) { - return index().session.all.get(session) ?? empty + return index.session.all[session] ?? empty }, unseen(session: string) { - return index().session.unseen.get(session) ?? empty + return index.session.unseen[session] ?? empty }, unseenCount(session: string) { - return index().session.unseenCount.get(session) ?? 0 + return index.session.unseenCount[session] ?? 0 }, unseenHasError(session: string) { - return index().session.unseenHasError.get(session) ?? false + return index.session.unseenHasError[session] ?? false }, markViewed(session: string) { - setStore("list", (n) => n.session === session, "viewed", true) + const unseen = index.session.unseen[session] ?? empty + if (!unseen.length) return + + const projects = [ + ...new Set(unseen.flatMap((notification) => (notification.directory ? [notification.directory] : []))), + ] + batch(() => { + setStore("list", (n) => n.session === session && !n.viewed, "viewed", true) + updateUnseen("session", session, []) + projects.forEach((directory) => { + const next = (index.project.unseen[directory] ?? empty).filter( + (notification) => notification.session !== session, + ) + updateUnseen("project", directory, next) + }) + }) }, }, project: { all(directory: string) { - return index().project.all.get(directory) ?? empty + return index.project.all[directory] ?? empty }, unseen(directory: string) { - return index().project.unseen.get(directory) ?? empty + return index.project.unseen[directory] ?? empty }, unseenCount(directory: string) { - return index().project.unseenCount.get(directory) ?? 0 + return index.project.unseenCount[directory] ?? 0 }, unseenHasError(directory: string) { - return index().project.unseenHasError.get(directory) ?? false + return index.project.unseenHasError[directory] ?? false }, markViewed(directory: string) { - setStore("list", (n) => n.directory === directory, "viewed", true) + const unseen = index.project.unseen[directory] ?? empty + if (!unseen.length) return + + const sessions = [ + ...new Set(unseen.flatMap((notification) => (notification.session ? [notification.session] : []))), + ] + batch(() => { + setStore("list", (n) => n.directory === directory && !n.viewed, "viewed", true) + updateUnseen("project", directory, []) + sessions.forEach((session) => { + const next = (index.session.unseen[session] ?? empty).filter( + (notification) => notification.directory !== directory, + ) + updateUnseen("session", session, next) + }) + }) }, }, } diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 0e6aa08cb..64f026219 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -101,11 +101,15 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str const all = store.all const index = all.findIndex((x) => x.id === id) if (index === -1) return - const filtered = all.filter((x) => x.id !== id) - const active = store.active === id ? filtered[0]?.id : store.active + const active = store.active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store.active batch(() => { - setStore("all", filtered) setStore("active", active) + setStore( + "all", + produce((draft) => { + draft.splice(index, 1) + }), + ) }) } @@ -157,10 +161,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str title: pty.data?.title ?? "Terminal", titleNumber: nextNumber, } - setStore("all", (all) => { - const newAll = [...all, newTerminal] - return newAll - }) + setStore("all", store.all.length, newTerminal) setStore("active", id) }) .catch((error: unknown) => { @@ -168,8 +169,11 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str }) }, update(pty: Partial<LocalPTY> & { id: string }) { - const previous = store.all.find((x) => x.id === pty.id) - if (previous) setStore("all", (all) => all.map((item) => (item.id === pty.id ? { ...item, ...pty } : item))) + const index = store.all.findIndex((x) => x.id === pty.id) + const previous = index >= 0 ? store.all[index] : undefined + if (index >= 0) { + setStore("all", index, (item) => ({ ...item, ...pty })) + } sdk.client.pty .update({ ptyID: pty.id, @@ -178,7 +182,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str }) .catch((error: unknown) => { if (previous) { - setStore("all", (all) => all.map((item) => (item.id === pty.id ? previous : item))) + const currentIndex = store.all.findIndex((item) => item.id === pty.id) + if (currentIndex >= 0) setStore("all", currentIndex, previous) } console.error("Failed to update terminal", error) }) @@ -232,15 +237,21 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str setStore("active", store.all[prevIndex]?.id) }, async close(id: string) { - batch(() => { - const filtered = store.all.filter((x) => x.id !== id) - if (store.active === id) { - const index = store.all.findIndex((f) => f.id === id) - const next = index > 0 ? index - 1 : 0 - setStore("active", filtered[next]?.id) - } - setStore("all", filtered) - }) + const index = store.all.findIndex((f) => f.id === id) + if (index !== -1) { + batch(() => { + if (store.active === id) { + const next = index > 0 ? store.all[index - 1]?.id : store.all[1]?.id + setStore("active", next) + } + setStore( + "all", + produce((all) => { + all.splice(index, 1) + }), + ) + }) + } await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => { console.error("Failed to close terminal", error) |
