Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
df51166
fix(ci): run V2 Discord notification after skipped build jobs (#52012)
opencode-agent[bot] Sep 29, 2026
760f87f
fix(core): pass through Copilot Responses settings (#52182)
rekram1-node Sep 29, 2026
74dbc50
fix(ai): place prompt cache breakpoints on OpenRouter Anthropic and Q…
rekram1-node Sep 29, 2026
1b02abc
feat(session-ui): group adjacent reads into one row (#52207)
Hona Sep 30, 2026
67b14e9
feat(session-ui): keep open Used group header sticky (#52210)
Hona Sep 30, 2026
bf97a04
fix(app): alert only for open session tabs (#52204)
opencode-agent[bot] Sep 30, 2026
a80a000
fix(ai): classify Workers AI context window rejections as context ove…
rekram1-node Sep 30, 2026
1243d09
tui: update OpenTUI to 0.5.13 (#52233)
simonklee Sep 30, 2026
dd68d6e
fix(app): select new-session project by resolved ID (#52230)
opencode-agent[bot] Sep 30, 2026
01efd22
feat(desktop): comment on page elements from the in-app browser (#52217)
Hona Sep 30, 2026
ffa4c4c
feat(app): open referenced session IDs from markdown (#52231)
opencode-agent[bot] Sep 30, 2026
c0285b4
chore(core): refresh bundled models.dev snapshot
opencode-agent[bot] Sep 30, 2026
1844c11
fix(acp): follow server defaults and refresh the session catalog (#52…
nexxeln Sep 30, 2026
02c3891
feat(core): let form cancellation carry a message (#52137)
jlongster Sep 30, 2026
9388b94
fix(tui): tighten expanded instruction group spacing (#52311)
OpeOginni Sep 30, 2026
0159356
tui: upgrade OpenTUI to 0.5.14 (#52316)
simonklee Sep 30, 2026
82bb5ff
fix(browser): hide browser tools unless a desktop is attached (#52309)
jlongster Sep 30, 2026
599bdc9
fix(ai): classify Amazon Nova input token rejections as context overf…
rekram1-node Sep 30, 2026
7878744
test(acp): drive ACP tests through the wire protocol (#52307)
nexxeln Sep 30, 2026
b020f10
fix(ai): route Cloudflare AI Gateway Claude and OpenAI through native…
rekram1-node Sep 30, 2026
14636b9
fix(core): avoid reinjecting ancestor instructions (#52364)
rekram1-node Sep 30, 2026
f46fa72
fix(core): add namespaced session identity headers (#52368)
rekram1-node Sep 30, 2026
91b9bc5
fix(ai): make model capability defaults forward-compatible (#52388)
rekram1-node Sep 30, 2026
a923596
feat(plugin): expose session removal (#52387)
rekram1-node Sep 30, 2026
dd2af0e
sync release versions for v2.0.21
Sep 30, 2026
210c16a
chore(sync): merge upstream v2 through v2.0.21
cursoragent Sep 30, 2026
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
Prev Previous commit
Next Next commit
fix(app): alert only for open session tabs (anomalyco#52204)
Co-authored-by: Brendonovich <[email protected]>
  • Loading branch information
opencode-agent[bot] and Brendonovich authored Sep 30, 2026
commit bf97a0443fc1c4f44178067aa2439d0a6b129617
94 changes: 94 additions & 0 deletions packages/app/src/shell/notifications/notification-alert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { afterAll, beforeAll, expect, mock, test } from "bun:test"
import { createRoot } from "solid-js"
import { createStore } from "solid-js/store"
import type { ServerSDK } from "@/runtime/server/client"
import type { Data } from "@opencode/client/solid"
import type { ServerConnection } from "@/runtime/server/registry"
import { ServerScope } from "@/runtime/server/scope"
import type { Tab } from "@/shell/tabs/tabs"

const server = "local\nhttp://localhost:4096" as ServerConnection.Key
const session = { id: "session-1", title: "Test session", location: { directory: "/project" } }
const alerts: string[] = []
const tabs: { store: Tab[] } = { store: [] }
let createServerNotificationState: typeof import("./notification").createServerNotificationState
let storage: typeof import("@/runtime/persistence/storage")

beforeAll(async () => {
storage = await import("@/runtime/persistence/storage")
const { sessionIDHasOpenTab } = await import("@/shell/tabs/tabs")
mock.module("@/runtime/platform/platform", () => ({
usePlatform: () => ({
platform: "web",
notify: async (title: string) => {
alerts.push(title)
},
}),
}))
mock.module("@/settings/model", () => ({
useSettings: () => ({
sounds: { agentEnabled: () => false, errorsEnabled: () => false },
notifications: { agent: () => true, errors: () => true },
}),
}))
mock.module("@/runtime/i18n/language", () => ({ useLanguage: () => ({ t: (key: string) => key }) }))
mock.module("@/shell/tabs/tabs", () => ({
useTabs: () => tabs,
sessionIDHasOpenTab,
}))
mock.module("@/runtime/persistence/storage", () => ({
...storage,
persisted: () => {
const [store, setStore] = createStore({ list: [] })
return [store, setStore, undefined, () => false]
},
}))
createServerNotificationState = (await import("./notification")).createServerNotificationState
})

afterAll(() => mock.module("@/runtime/persistence/storage", () => storage))

test.each([
["session.execution.succeeded", "notification.session.responseReady.title"],
["session.execution.failed", "notification.session.error.title"],
] as const)("system alert for %s requires an open session tab", async (type, title) => {
alerts.length = 0
tabs.store = [{ type: "session", server, sessionId: "another-session" }]
let listener: ((event: unknown) => void) | undefined
const dispose = createRoot((dispose) => {
const state = createServerNotificationState({
key: server,
sdk: {
scope: ServerScope.local,
event: {
listen: (fn: typeof listener) => {
listener = fn
return () => {}
},
},
} as unknown as ServerSDK,
data: { session: { get: () => session } } as unknown as Data,
coordinator: { system: async (_id: string, fn: () => Promise<void>) => fn() },
} as Parameters<typeof createServerNotificationState>[0])
return { dispose, state }
})

listener?.({
type,
id: "event-1",
data: { sessionID: session.id, error: { type: "api", message: "failed", status: 500 } },
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(dispose.state.session.all(session.id)).toHaveLength(1)
expect(alerts).toEqual([])

tabs.store = [{ type: "session", server, sessionId: session.id }]
listener?.({
type,
id: "event-2",
data: { sessionID: session.id, error: { type: "api", message: "failed", status: 500 } },
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(alerts).toEqual([title])
dispose.dispose()
})
10 changes: 6 additions & 4 deletions packages/app/src/shell/notifications/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ export function createServerNotificationState(input: {
if (!session) return
if (session.parentID) return

if (sessionIDHasOpenTab(tabs.store, input.key, sessionID) && settings.sounds.agentEnabled()) {
const hasOpenTab = sessionIDHasOpenTab(tabs.store, input.key, sessionID)
if (hasOpenTab && settings.sounds.agentEnabled()) {
void input.coordinator.sound(`${input.key}\0${eventID}`, () => playSoundById(settings.sounds.agent()))
}

Expand All @@ -240,7 +241,7 @@ export function createServerNotificationState(input: {
session: sessionID,
})

if (settings.notifications.agent()) {
if (hasOpenTab && settings.notifications.agent()) {
void input.coordinator.system(`${input.key}\0${eventID}`, () =>
platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, () =>
openNotificationSession(tabs, input.key, sessionID),
Expand All @@ -255,7 +256,8 @@ export function createServerNotificationState(input: {
if (meta.disposed) return
if (session?.parentID) return

if (sessionIDHasOpenTab(tabs.store, input.key, sessionID) && settings.sounds.errorsEnabled()) {
const hasOpenTab = sessionIDHasOpenTab(tabs.store, input.key, sessionID)
if (hasOpenTab && settings.sounds.errorsEnabled()) {
void input.coordinator.sound(`${input.key}\0${eventID}`, () => playSoundById(settings.sounds.errors()))
}

Expand All @@ -270,7 +272,7 @@ export function createServerNotificationState(input: {
const description =
session?.title ??
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
if (settings.notifications.errors()) {
if (hasOpenTab && settings.notifications.errors()) {
void input.coordinator.system(`${input.key}\0${eventID}`, () =>
platform.notify(language.t("notification.session.error.title"), description, () =>
openNotificationSession(tabs, input.key, sessionID),
Expand Down