π File detail
services/notifier.ts
π― Use case
This file lives under βservices/β, which covers long-lived services (LSP, MCP, OAuth, tool execution, memory, compaction, voice, settings sync, β¦). On the API surface it exposes NotificationOptions and sendNotification β mainly functions, hooks, or classes. It composes internal code from ink, utils, and analytics (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import type { TerminalNotification } from '../ink/useTerminalNotification.js' import { getGlobalConfig } from '../utils/config.js' import { env } from '../utils/env.js' import { execFileNoThrow } from '../utils/execFileNoThrow.js' import { executeNotificationHooks } from '../utils/hooks.js'
π€ Exports (heuristic)
NotificationOptionssendNotification
π₯οΈ Source preview
import type { TerminalNotification } from '../ink/useTerminalNotification.js'
import { getGlobalConfig } from '../utils/config.js'
import { env } from '../utils/env.js'
import { execFileNoThrow } from '../utils/execFileNoThrow.js'
import { executeNotificationHooks } from '../utils/hooks.js'
import { logError } from '../utils/log.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from './analytics/index.js'
export type NotificationOptions = {
message: string
title?: string
notificationType: string
}
export async function sendNotification(
notif: NotificationOptions,
terminal: TerminalNotification,
): Promise<void> {
const config = getGlobalConfig()
const channel = config.preferredNotifChannel
await executeNotificationHooks(notif)
const methodUsed = await sendToChannel(channel, notif, terminal)
logEvent('tengu_notification_method_used', {
configured_channel:
channel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
method_used:
methodUsed as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
term: env.terminal as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
const DEFAULT_TITLE = 'Claude Code'
async function sendToChannel(
channel: string,
opts: NotificationOptions,
terminal: TerminalNotification,
): Promise<string> {
const title = opts.title || DEFAULT_TITLE
try {
switch (channel) {
case 'auto':
return sendAuto(opts, terminal)
case 'iterm2':
terminal.notifyITerm2(opts)
return 'iterm2'
case 'iterm2_with_bell':
terminal.notifyITerm2(opts)
terminal.notifyBell()
return 'iterm2_with_bell'
case 'kitty':
terminal.notifyKitty({ ...opts, title, id: generateKittyId() })
return 'kitty'
case 'ghostty':
terminal.notifyGhostty({ ...opts, title })
return 'ghostty'
case 'terminal_bell':
terminal.notifyBell()
return 'terminal_bell'
case 'notifications_disabled':
return 'disabled'
default:
return 'none'
}
} catch {
return 'error'
}
}
async function sendAuto(
opts: NotificationOptions,
terminal: TerminalNotification,
): Promise<string> {
const title = opts.title || DEFAULT_TITLE
switch (env.terminal) {
case 'Apple_Terminal': {
const bellDisabled = await isAppleTerminalBellDisabled()
if (bellDisabled) {
terminal.notifyBell()
return 'terminal_bell'
}
return 'no_method_available'
}
case 'iTerm.app':
terminal.notifyITerm2(opts)
return 'iterm2'
case 'kitty':
terminal.notifyKitty({ ...opts, title, id: generateKittyId() })
return 'kitty'
case 'ghostty':
terminal.notifyGhostty({ ...opts, title })
return 'ghostty'
default:
return 'no_method_available'
}
}
function generateKittyId(): number {
return Math.floor(Math.random() * 10000)
}
async function isAppleTerminalBellDisabled(): Promise<boolean> {
try {
if (env.terminal !== 'Apple_Terminal') {
return false
}
const osascriptResult = await execFileNoThrow('osascript', [
'-e',
'tell application "Terminal" to name of current settings of front window',
])
const currentProfile = osascriptResult.stdout.trim()
if (!currentProfile) {
return false
}
const defaultsOutput = await execFileNoThrow('defaults', [
'export',
'com.apple.Terminal',
'-',
])
if (defaultsOutput.code !== 0) {
return false
}
// Lazy-load plist (~280KB with xmlbuilder+@xmldom) β only hit on
// Apple_Terminal with auto-channel, which is a small fraction of users.
const plist = await import('plist')
const parsed: Record<string, unknown> = plist.parse(defaultsOutput.stdout)
const windowSettings = parsed?.['Window Settings'] as
| Record<string, unknown>
| undefined
const profileSettings = windowSettings?.[currentProfile] as
| Record<string, unknown>
| undefined
if (!profileSettings) {
return false
}
return profileSettings.Bell === false
} catch (error) {
logError(error)
return false
}
}