fix(app): centralize notification state (#34105)
This commit is contained in:
@@ -38,7 +38,7 @@ import { HighlightsProvider } from "@/context/highlights"
|
|||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
import { ModelsProvider } from "@/context/models"
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { NotificationProvider } from "@/context/notification"
|
import { NotificationProvider, useNotification } from "@/context/notification"
|
||||||
import { PermissionProvider } from "@/context/permission"
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
@@ -316,9 +316,7 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
|
|||||||
return (
|
return (
|
||||||
<PermissionProvider directory={props.directory}>
|
<PermissionProvider directory={props.directory}>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<NotificationProvider directory={props.directory} sessionID={props.sessionID}>
|
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
</NotificationProvider>
|
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
@@ -345,13 +343,23 @@ function NewAppLayout(props: ParentProps) {
|
|||||||
function TargetServerScopedProviders(props: ServerScopedShellProps) {
|
function TargetServerScopedProviders(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<PermissionProvider directory={props.directory}>
|
<PermissionProvider directory={props.directory}>
|
||||||
<NotificationProvider directory={props.directory} sessionID={props.sessionID}>
|
<MarkSessionNotificationsViewed sessionID={props.sessionID} />
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
</NotificationProvider>
|
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) {
|
||||||
|
const notification = useNotification()
|
||||||
|
createEffect(() => {
|
||||||
|
const sessionID = props.sessionID?.()
|
||||||
|
if (!notification.ready() || !sessionID) return
|
||||||
|
if (notification.session.unseenCount(sessionID) === 0) return
|
||||||
|
notification.session.markViewed(sessionID)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function SessionProviders(props: ParentProps) {
|
function SessionProviders(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<TerminalProvider>
|
<TerminalProvider>
|
||||||
@@ -560,11 +568,13 @@ export function AppInterface(props: {
|
|||||||
component={props.router ?? Router}
|
component={props.router ?? Router}
|
||||||
root={(routerProps) => (
|
root={(routerProps) => (
|
||||||
<TabsProvider>
|
<TabsProvider>
|
||||||
|
<NotificationProvider>
|
||||||
<ServerShell>
|
<ServerShell>
|
||||||
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||||
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
<NewAppLayout>{routerProps.children}</NewAppLayout>
|
||||||
</Show>
|
</Show>
|
||||||
</ServerShell>
|
</ServerShell>
|
||||||
|
</NotificationProvider>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
|
import { type Accessor, batch, createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useServerSDK } from "./server-sdk"
|
import type { ServerSDK } from "./server-sdk"
|
||||||
import { useServerSync } from "./server-sync"
|
import type { ServerSync } from "./server-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
@@ -12,6 +12,11 @@ import { decode64 } from "@/utils/base64"
|
|||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { playSoundById } from "@/utils/sound"
|
import { playSoundById } from "@/utils/sound"
|
||||||
|
import { useGlobal } from "./global"
|
||||||
|
import { ServerConnection, useServer } from "./server"
|
||||||
|
import { type DraftTab, useTabs } from "./tabs"
|
||||||
|
import { requireServerKey } from "@/utils/session-route"
|
||||||
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
|
|
||||||
type NotificationBase = {
|
type NotificationBase = {
|
||||||
directory?: string
|
directory?: string
|
||||||
@@ -107,21 +112,115 @@ function buildNotificationIndex(list: Notification[]) {
|
|||||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||||
name: "Notification",
|
name: "Notification",
|
||||||
gate: false,
|
gate: false,
|
||||||
init: (props: { directory?: Accessor<string | undefined>; sessionID?: Accessor<string | undefined> }) => {
|
init: () => {
|
||||||
const params = useParams()
|
const params = useParams<{ serverKey?: string; dir?: string; id?: string }>()
|
||||||
const serverSDK = useServerSDK()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const serverSync = useServerSync()
|
const global = useGlobal()
|
||||||
|
const server = useServer()
|
||||||
|
const tabs = useTabs()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const owner = getOwner()
|
||||||
|
const states = new Map<ServerScope, { dispose: () => void; state: NotificationState }>()
|
||||||
|
|
||||||
|
const activeServer = createMemo(() => {
|
||||||
|
if (params.serverKey) return requireServerKey(params.serverKey)
|
||||||
|
if (search.draftId) {
|
||||||
|
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)
|
||||||
|
if (draft) return draft.server
|
||||||
|
}
|
||||||
|
return server.key
|
||||||
|
})
|
||||||
|
const activeDirectory = createMemo(() => decode64(params.dir))
|
||||||
|
const activeSession = createMemo(() => params.id)
|
||||||
|
|
||||||
|
const ensure = (key: ServerConnection.Key) => {
|
||||||
|
const conn = global.servers.list().find((item) => ServerConnection.key(item) === key)
|
||||||
|
if (!conn) throw new Error(`Notification server not found: ${key}`)
|
||||||
|
const ctx = global.ensureServerCtx(conn)
|
||||||
|
const existing = states.get(ctx.sdk.scope)
|
||||||
|
if (existing) return existing.state
|
||||||
|
const root = createRoot(
|
||||||
|
(dispose) => ({
|
||||||
|
dispose,
|
||||||
|
state: createServerNotificationState({
|
||||||
|
sdk: ctx.sdk,
|
||||||
|
sync: ctx.sync,
|
||||||
|
active: () => server.scope(activeServer()) === ctx.sdk.scope,
|
||||||
|
directory: activeDirectory,
|
||||||
|
sessionID: activeSession,
|
||||||
|
platform,
|
||||||
|
settings,
|
||||||
|
language,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
owner ?? undefined,
|
||||||
|
)
|
||||||
|
states.set(ctx.sdk.scope, root)
|
||||||
|
return root.state
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
global.servers.list().forEach((conn) => ensure(ServerConnection.key(conn)))
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const scopes = new Set(global.servers.list().map((conn) => server.scope(ServerConnection.key(conn))))
|
||||||
|
states.forEach((value, scope) => {
|
||||||
|
if (scopes.has(scope)) return
|
||||||
|
value.dispose()
|
||||||
|
states.delete(scope)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => states.forEach((value) => value.dispose()))
|
||||||
|
|
||||||
|
const selected = () => ensure(activeServer())
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: () => selected().ready(),
|
||||||
|
ensureServerState: ensure,
|
||||||
|
session: {
|
||||||
|
all: (session: string) => selected().session.all(session),
|
||||||
|
unseen: (session: string) => selected().session.unseen(session),
|
||||||
|
unseenCount: (session: string) => selected().session.unseenCount(session),
|
||||||
|
unseenHasError: (session: string) => selected().session.unseenHasError(session),
|
||||||
|
markViewed: (session: string) => selected().session.markViewed(session),
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
all: (directory: string) => selected().project.all(directory),
|
||||||
|
unseen: (directory: string) => selected().project.unseen(directory),
|
||||||
|
unseenCount: (directory: string) => selected().project.unseenCount(directory),
|
||||||
|
unseenHasError: (directory: string) => selected().project.unseenHasError(directory),
|
||||||
|
markViewed: (directory: string) => selected().project.markViewed(directory),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type NotificationState = ReturnType<typeof createServerNotificationState>
|
||||||
|
|
||||||
|
function createServerNotificationState(input: {
|
||||||
|
sdk: ServerSDK
|
||||||
|
sync: ServerSync
|
||||||
|
active: Accessor<boolean>
|
||||||
|
directory: Accessor<string | undefined>
|
||||||
|
sessionID: Accessor<string | undefined>
|
||||||
|
platform: ReturnType<typeof usePlatform>
|
||||||
|
settings: ReturnType<typeof useSettings>
|
||||||
|
language: ReturnType<typeof useLanguage>
|
||||||
|
}) {
|
||||||
|
const serverSDK = () => input.sdk
|
||||||
|
const serverSync = () => input.sync
|
||||||
|
const platform = input.platform
|
||||||
|
const settings = input.settings
|
||||||
|
const language = input.language
|
||||||
|
|
||||||
const empty: Notification[] = []
|
const empty: Notification[] = []
|
||||||
|
|
||||||
const currentDirectory = createMemo(() => {
|
const currentDirectory = input.directory
|
||||||
return props.directory?.() ?? decode64(params.dir)
|
const currentSession = input.sessionID
|
||||||
})
|
|
||||||
|
|
||||||
const currentSession = createMemo(() => props.sessionID?.() ?? params.id)
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
|
Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
|
||||||
@@ -217,12 +316,12 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const viewedInCurrentSession = (directory: string, sessionID?: string) => {
|
const viewedInCurrentSession = (directory: string, sessionID?: string) => {
|
||||||
|
if (!input.active()) return false
|
||||||
const activeDirectory = currentDirectory()
|
const activeDirectory = currentDirectory()
|
||||||
const activeSession = currentSession()
|
const activeSession = currentSession()
|
||||||
if (!activeDirectory) return false
|
|
||||||
if (!activeSession) return false
|
if (!activeSession) return false
|
||||||
if (!sessionID) return false
|
if (!sessionID) return false
|
||||||
if (directory !== activeDirectory) return false
|
if (activeDirectory && directory !== activeDirectory) return false
|
||||||
return sessionID === activeSession
|
return sessionID === activeSession
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,5 +468,4 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
|||||||
@@ -341,15 +341,15 @@ export function NewHome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unseenCount(conn: ServerConnection.Any, project: LocalProject) {
|
function unseenCount(conn: ServerConnection.Any, project: LocalProject) {
|
||||||
if (ServerConnection.key(conn) !== server.key) return 0
|
const state = notification.ensureServerState(ServerConnection.key(conn))
|
||||||
return directories(project).reduce((total, directory) => total + notification.project.unseenCount(directory), 0)
|
return directories(project).reduce((total, directory) => total + state.project.unseenCount(directory), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearNotifications(conn: ServerConnection.Any, project: LocalProject) {
|
function clearNotifications(conn: ServerConnection.Any, project: LocalProject) {
|
||||||
if (ServerConnection.key(conn) !== server.key) return
|
const state = notification.ensureServerState(ServerConnection.key(conn))
|
||||||
directories(project)
|
directories(project)
|
||||||
.filter((directory) => notification.project.unseenCount(directory) > 0)
|
.filter((directory) => state.project.unseenCount(directory) > 0)
|
||||||
.forEach((directory) => notification.project.markViewed(directory))
|
.forEach((directory) => state.project.markViewed(directory))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSession(session: Session) {
|
function openSession(session: Session) {
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { HelpButton } from "@/components/help-button"
|
import { HelpButton } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
import { useNotification } from "@/context/notification"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
import { setV2Toast, ToastRegion } from "@/utils/toast"
|
import { setV2Toast, ToastRegion } from "@/utils/toast"
|
||||||
|
|
||||||
export default function NewLayout(props: ParentProps) {
|
export default function NewLayout(props: ParentProps) {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const notification = useNotification()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const params = useParams<{ id?: string }>()
|
|
||||||
setNavigate(navigate)
|
setNavigate(navigate)
|
||||||
|
|
||||||
createEffect(() => setV2Toast(true))
|
createEffect(() => setV2Toast(true))
|
||||||
createEffect(() => {
|
|
||||||
if (!notification.ready() || !params.id) return
|
|
||||||
if (notification.session.unseenCount(params.id) === 0) return
|
|
||||||
notification.session.markViewed(params.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
const update: TitlebarUpdate = {
|
const update: TitlebarUpdate = {
|
||||||
version: () => {
|
version: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user