File size: 4,256 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | 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
}
}
|