fix(session-notification): defer platform detection and background checks

This commit is contained in:
Sisyphus
2026-04-18 14:13:12 +09:00
parent 439957c4cd
commit ed6ac7ea96
2 changed files with 69 additions and 56 deletions

View File

@@ -0,0 +1,31 @@
import type { Platform } from "./session-notification-sender"
import * as sessionNotificationSender from "./session-notification-sender"
import { startBackgroundCheck } from "./session-notification-utils"
export function createSessionNotificationInit() {
let platform: Platform | null = null
let defaultSoundPath: string | null = null
let started = false
function initialize(): { platform: Platform; defaultSoundPath: string } {
if (!platform) {
platform = sessionNotificationSender.detectPlatform()
}
if (!defaultSoundPath) {
defaultSoundPath = sessionNotificationSender.getDefaultSoundPath(platform)
}
if (!started) {
startBackgroundCheck(platform)
started = true
}
return {
platform,
defaultSoundPath,
}
}
return {
initialize,
}
}

View File

@@ -1,20 +1,12 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { subagentSessions, getMainSessionID } from "../features/claude-code-session-state"
import {
startBackgroundCheck,
} from "./session-notification-utils"
import { buildReadyNotificationContent } from "./session-notification-content"
import {
type Platform,
} from "./session-notification-sender"
import { type Platform } from "./session-notification-sender"
import * as sessionNotificationSender from "./session-notification-sender"
import {
getEventToolName,
getQuestionText,
getSessionID,
} from "./session-notification-event-properties"
import { getEventToolName, getQuestionText, getSessionID } from "./session-notification-event-properties"
import { hasIncompleteTodos } from "./session-todo-status"
import { createIdleNotificationScheduler } from "./session-notification-scheduler"
import { createSessionNotificationInit } from "./session-notification-init"
interface SessionNotificationConfig {
title?: string
@@ -33,22 +25,15 @@ interface SessionNotificationConfig {
/** Grace period in ms to ignore late-arriving activity events after scheduling (default: 100) */
activityGracePeriodMs?: number
}
export function createSessionNotification(
ctx: PluginInput,
config: SessionNotificationConfig = {}
) {
const currentPlatform: Platform = sessionNotificationSender.detectPlatform()
const defaultSoundPath = sessionNotificationSender.getDefaultSoundPath(currentPlatform)
startBackgroundCheck(currentPlatform)
export function createSessionNotification(ctx: PluginInput, config: SessionNotificationConfig = {}) {
const mergedConfig = {
title: "OpenCode",
message: "Agent is ready for input",
questionMessage: "Agent is asking a question",
permissionMessage: "Agent needs permission to continue",
playSound: false,
soundPath: defaultSoundPath,
soundPath: "",
idleConfirmationDelay: 1500,
skipIfIncompleteTodos: true,
maxTrackedSessions: 100,
@@ -56,22 +41,18 @@ export function createSessionNotification(
...config,
}
const sessionNotificationInit = createSessionNotificationInit()
let currentPlatform: Platform | null = null
let defaultSoundPath = mergedConfig.soundPath
const scheduler = createIdleNotificationScheduler({
ctx,
platform: currentPlatform,
platform: "unsupported",
config: mergedConfig,
hasIncompleteTodos,
send: async (hookCtx, platform, sessionID) => {
if (
typeof hookCtx.client.session.get !== "function"
&& typeof hookCtx.client.session.messages !== "function"
) {
await sessionNotificationSender.sendSessionNotification(
hookCtx,
platform,
mergedConfig.title,
mergedConfig.message,
)
if (typeof hookCtx.client.session.get !== "function" && typeof hookCtx.client.session.messages !== "function") {
await sessionNotificationSender.sendSessionNotification(hookCtx, platform, mergedConfig.title, mergedConfig.message)
return
}
@@ -90,6 +71,15 @@ export function createSessionNotification(
const PERMISSION_EVENTS = new Set(["permission.ask", "permission.asked", "permission.updated", "permission.requested"])
const PERMISSION_HINT_PATTERN = /\b(permission|approve|approval|allow|deny|consent)\b/i
const ensureNotificationPlatform = (): Platform => {
if (currentPlatform) return currentPlatform
const initialized = sessionNotificationInit.initialize()
currentPlatform = initialized.platform
defaultSoundPath = initialized.defaultSoundPath || mergedConfig.soundPath
return currentPlatform
}
const shouldNotifyForSession = (sessionID: string): boolean => {
if (subagentSessions.has(sessionID)) return false
@@ -102,16 +92,12 @@ export function createSessionNotification(
}
return async ({ event }: { event: { type: string; properties?: unknown } }) => {
if (currentPlatform === "unsupported") return
const props = event.properties as Record<string, unknown> | undefined
if (event.type === "session.created") {
const info = props?.info as Record<string, unknown> | undefined
const sessionID = info?.id as string | undefined
if (sessionID) {
scheduler.markSessionActivity(sessionID)
}
if (sessionID) scheduler.markSessionActivity(sessionID)
return
}
@@ -119,6 +105,8 @@ export function createSessionNotification(
const sessionID = getSessionID(props)
if (!sessionID) return
const platform = ensureNotificationPlatform()
if (platform === "unsupported") return
if (!shouldNotifyForSession(sessionID)) return
scheduler.scheduleIdleNotification(sessionID)
@@ -128,26 +116,22 @@ export function createSessionNotification(
if (event.type === "message.updated") {
const info = props?.info as Record<string, unknown> | undefined
const sessionID = getSessionID({ ...props, info })
if (sessionID) {
scheduler.markSessionActivity(sessionID)
}
if (sessionID) scheduler.markSessionActivity(sessionID)
return
}
if (PERMISSION_EVENTS.has(event.type)) {
const sessionID = getSessionID(props)
if (!sessionID) return
const platform = ensureNotificationPlatform()
if (platform === "unsupported") return
if (!shouldNotifyForSession(sessionID)) return
scheduler.markSessionActivity(sessionID)
await sessionNotificationSender.sendSessionNotification(
ctx,
currentPlatform,
mergedConfig.title,
mergedConfig.permissionMessage,
)
if (mergedConfig.playSound && mergedConfig.soundPath) {
await sessionNotificationSender.playSessionNotificationSound(ctx, currentPlatform, mergedConfig.soundPath)
await sessionNotificationSender.sendSessionNotification(ctx, platform, mergedConfig.title, mergedConfig.permissionMessage)
if (mergedConfig.playSound && defaultSoundPath) {
await sessionNotificationSender.playSessionNotificationSound(ctx, platform, defaultSoundPath)
}
return
}
@@ -160,16 +144,16 @@ export function createSessionNotification(
if (event.type === "tool.execute.before") {
const toolName = getEventToolName(props)?.toLowerCase()
if (toolName && QUESTION_TOOLS.has(toolName)) {
const platform = ensureNotificationPlatform()
if (platform === "unsupported") return
if (!shouldNotifyForSession(sessionID)) return
const questionText = getQuestionText(props)
const message = PERMISSION_HINT_PATTERN.test(questionText)
? mergedConfig.permissionMessage
: mergedConfig.questionMessage
const message = PERMISSION_HINT_PATTERN.test(questionText) ? mergedConfig.permissionMessage : mergedConfig.questionMessage
await sessionNotificationSender.sendSessionNotification(ctx, currentPlatform, mergedConfig.title, message)
if (mergedConfig.playSound && mergedConfig.soundPath) {
await sessionNotificationSender.playSessionNotificationSound(ctx, currentPlatform, mergedConfig.soundPath)
await sessionNotificationSender.sendSessionNotification(ctx, platform, mergedConfig.title, message)
if (mergedConfig.playSound && defaultSoundPath) {
await sessionNotificationSender.playSessionNotificationSound(ctx, platform, defaultSoundPath)
}
}
}
@@ -179,9 +163,7 @@ export function createSessionNotification(
if (event.type === "session.deleted") {
const sessionInfo = props?.info as { id?: string } | undefined
if (sessionInfo?.id) {
scheduler.deleteSession(sessionInfo.id)
}
if (sessionInfo?.id) scheduler.deleteSession(sessionInfo.id)
}
}
}