Skip to content

Принцип обнаружения терминала: механизм автоматического распознавания и обнаружение фокуса macOS

Что вы сможете сделать после изучения

  • Глубоко понять механизм работы и принципы реализации автоматического обнаружения терминала
  • Освоить технические детали обнаружения фокуса и динамического получения Bundle ID в macOS
  • Научиться вручную переопределять тип терминала для решения проблем сбоев обнаружения
  • Понимать различия обнаружения на разных платформах (macOS/Windows/Linux)
  • Разбираться в реализации исходного кода, связанного с обнаружением терминала

Ваши текущие затруднения

Вы уже знаете, что opencode-notify поддерживает 37+ терминалов, и также знаете, что можете вручную указать тип терминала. Но вы хотите глубоко понять:

  • Как реализовано обнаружение терминала на низком уровне?
  • Через какой механизм реализовано обнаружение фокуса в macOS?
  • Почему macOS необходимо получать Bundle ID, а Windows/Linux не нужно?
  • Как плагин обрабатывает ситуацию, когда автоматическое обнаружение не удалось?
  • Как распознаются мультиплексоры терминалов, такие как tmux?

В этом уроке мы проведем вас через исходный код, чтобы объяснить эти технические детали.

Основная идея

Реализация обнаружения терминала делится на три уровня:

1. Уровень автоматического обнаружения: библиотека detect-terminal распознает тип терминала

2. Уровень адаптации платформы: специфическая обработка для macOS/Windows/Linux

3. Уровень расширения функциональности: обнаружение фокуса, нажатие для фокусировки, получение Bundle ID

Ключевые принципы проектирования:

  • Обнаружение один раз при запуске: плагин обнаруживает терминал при запуске и кэширует результат для последующих уведомлений (оптимизация производительности)
  • Приоритет ручной конфигурации: ручная настройка terminal имеет приоритет над автоматическим обнаружением
  • Понижение функциональности при сбое: когда обнаружение не удалось, отправка уведомлений не блокируется, просто отключается функция интеллектуальной фильтрации
  • Адаптация к различиям платформ: macOS поддерживает полную функциональность, Windows/Linux поддерживают базовые уведомления

Почему обнаружение при запуске?

Обнаружение терминала включает сканирование процессов, вызовы osascript и другие операции. Если выполнять обнаружение перед каждым уведомлением, это вызовет значительные накладные расходы. Поэтому плагин обнаруживает один раз при запуске, кэширует объект TerminalInfo, и все последующие уведомления повторно используют этот кэш.

Полный процесс обнаружения терминала

Анализ исходного кода: detectTerminalInfo()

Давайте проанализируем функцию основного обнаружения построчно:

typescript
// src/notify.ts:145-164
async function detectTerminalInfo(config: NotifyConfig): Promise<TerminalInfo> {
    // Использовать переопределение конфигурации, если предоставлено
    const terminalName = config.terminal || detectTerminal() || null

    if (!terminalName) {
        return { name: null, bundleId: null, processName: null }
    }

    // Получить имя процесса для обнаружения фокуса
    const processName = TERMINAL_PROCESS_NAMES[terminalName.toLowerCase()] || terminalName

    // Динамически получить Bundle ID из macOS (без хардкода!)
    const bundleId = await getBundleId(processName)

    return {
        name: terminalName,
        bundleId,
        processName,
    }
}

Разбор шагов:

ШагКодОписание
1config.terminal || detectTerminal()Приоритет использования переопределения конфигурации, иначе автоматическое обнаружение
2!terminalName ? return {...}Возврат пустого объекта при сбое обнаружения
3TERMINAL_PROCESS_NAMES[...]Поиск в таблице соответствия для получения имени процесса macOS
4await getBundleId()Динамическое получение Bundle ID в macOS
5return { name, bundleId, processName }Возврат полной информации о терминале

Структура возвращаемого TerminalInfo:

typescript
interface TerminalInfo {
    name: string | null       // Название терминала, например "ghostty"
    bundleId: string | null   // Bundle ID macOS, например "com.mitchellh.ghostty"
    processName: string | null // Имя процесса macOS, например "Ghostty"
}
Зачем macOS нужны три поля?
  • name: Общее название терминала (возвращается из detect-terminal)
  • processName: Имя процесса в диспетчере процессов macOS (используется для обнаружения фокуса)
  • bundleId: Уникальный идентификатор приложения macOS (используется для нажатия фокусировки)

Например:

  • name = "ghostty" (возвращает detect-terminal)
  • processName = "Ghostty" (поиск в таблице соответствия)
  • bundleId = "com.mitchellh.ghostty" (динамический запрос osascript)

Принцип работы библиотеки detect-terminal

Что такое detect-terminal?

detect-terminal — это библиотека Node.js, специально предназначенная для идентификации эмуляторов терминала. Она идентифицирует текущий запущенный терминал путем сканирования системных процессов.

Методы обнаружения:

  1. Сканирование процессов: проверка цепочки родительских процессов текущего процесса
  2. Обнаружение переменных окружения: проверка переменных окружения TERM, TERM_PROGRAM и др.
  3. Сопоставление с известными именами процессов: сравнение с именами процессов 37+ известных терминалов

Поддерживаемые способы обнаружения:

typescript
// Внутренняя логика detect-terminal (упрощенная)
function detect() {
    // 1. Проверка переменных окружения
    if (process.env.TERM_PROGRAM) {
        return process.env.TERM_PROGRAM
    }

    // 2. Сканирование цепочки родительских процессов
    const parentProcess = getParentProcess()
    if (isKnownTerminal(parentProcess.name)) {
        return parentProcess.name
    }

    // 3. Проверка специфических переменных окружения
    if (process.env.TMUX) return 'tmux'
    if (process.env.VSCODE_PID) return 'vscode'

    return null
}

Количество поддерживаемых терминалов: 37+ (см. полный список)

Специальная обработка macOS

1. Таблица соответствия имен процессов

В исходном коде определено отображение имен терминалов на имена процессов macOS:

typescript
// src/notify.ts:71-84
const TERMINAL_PROCESS_NAMES: Record<string, string> = {
    ghostty: "Ghostty",
    kitty: "kitty",
    iterm: "iTerm2",
    iterm2: "iTerm2",
    wezterm: "WezTerm",
    alacritty: "Alacritty",
    terminal: "Terminal",
    apple_terminal: "Terminal",
    hyper: "Hyper",
    warp: "Warp",
    vscode: "Code",
    "vscode-insiders": "Code - Insiders",
}

Зачем нужна таблица соответствия?

  • Имя терминала, возвращаемое detect-terminal, может быть в нижнем регистре или сокращенным (например, "ghostty")
  • Имя процесса в диспетчере процессов macOS может отличаться регистром (например, "Ghostty")
  • Некоторые терминалы имеют несколько вариантов записи (например, "iterm" и "iterm2" оба соответствуют "iTerm2")

Логика отображения:

typescript
const processName = TERMINAL_PROCESS_NAMES[terminalName.toLowerCase()] || terminalName
  • Сначала имя терминала преобразуется в нижний регистр для поиска в таблице
  • Если найдено, используется отображенное имя процесса
  • Если не найдено, используется исходное имя терминала

2. Динамическое получение Bundle ID

Реализация исходного кода:

typescript
// src/notify.ts:135-137
async function getBundleId(appName: string): Promise<string | null> {
    return runOsascript(`id of application "${appName}"`)
}

Команда osascript:

applescript
id of application "Ghostty"
-- Возвращает: com.mitchellh.ghostty

Почему не хардкодить Bundle ID?

Недостатки хардкода:

  • ❌ Bundle ID может измениться при обновлении терминала
  • ❌ Требуется ручное поддержание списка Bundle ID для 37+ терминалов
  • ❌ Код нужно обновлять при выпуске новых терминалов

Преимущества динамического получения:

  • ✅ Автоматическая адаптация к изменениям версий терминалов
  • ✅ Снижение затрат на поддержку
  • ✅ Теоретическая поддержка любого терминала macOS

3. Реализация обнаружения фокуса

Реализация исходного кода:

typescript
// src/notify.ts:139-143
async function getFrontmostApp(): Promise<string | null> {
    return runOsascript(
        'tell application "System Events" to get name of first application process whose frontmost is true',
    )
}

// src/notify.ts:166-175
async function isTerminalFocused(terminalInfo: TerminalInfo): Promise<boolean> {
    if (!terminalInfo.processName) return false
    if (process.platform !== "darwin") return false

    const frontmost = await getFrontmostApp()
    if (!frontmost) return false

    // Сравнение без учета регистра
    return frontmost.toLowerCase() === terminalInfo.processName.toLowerCase()
}

Команда osascript:

applescript
tell application "System Events" to get name of first application process whose frontmost is true
-- Возвращает: Ghostty

Процесс обнаружения:

1. Проверка платформы: для не-macOS сразу возвращается false

2. Проверка processName: без имени процесса сразу возвращается false

3. Получение активного приложения: запрос через osascript

4. Сравнение имен процессов: без учета регистра

5. Возврат результата: совпадает — сфокусирован, не совпадает — не сфокусирован

Использование в процессе отправки уведомлений:

typescript
// src/notify.ts:264-265
// Проверить, сфокирован ли терминал (подавить уведомление, если пользователь уже смотрит)
if (await isTerminalFocused(terminalInfo)) return

Когда терминал на переднем плане, плагин подавляет отправку уведомления, чтобы избежать повторных напоминаний.

Механизм переопределения конфигурации

Когда нужно переопределять вручную?

Применимые сценарии:

  1. Сбой автоматического обнаружения: detect-terminal не может распознать ваш терминал
  2. Сценарии вложенных терминалов: использование Alacritty в tmux, хотите распознать как tmux
  3. Неточность результатов обнаружения: ошибочно распознается как другой терминал

Метод конфигурации

Файл конфигурации: ~/.config/opencode/kdco-notify.json

json
{
  "terminal": "ghostty"
}

Логика приоритета (строка 147 исходного кода):

typescript
const terminalName = config.terminal || detectTerminal() || null

Порядок приоритета:

  1. Переопределение конфигурации (config.terminal): наивысший приоритет
  2. Автоматическое обнаружение (detectTerminal()): второй приоритет
  3. Обработка понижения (null): используется null при сбое обнаружения

Сценарии применения переопределения конфигурации

Если ваш терминал может быть правильно распознан, не рекомендуется переопределять вручную. Переопределение конфигурации в основном используется для решения проблем со сбоями обнаружения или специальных сценариев.

Сравнение различий платформ

macOS

ФункцияПоддержкаСпособ реализации
Системное уведомлениеnode-notifier (NSUserNotificationCenter)
Обнаружение терминалабиблиотека detect-terminal
Обнаружение фокусаosascript запрашивает активное приложение
Нажатие для фокусировкиnode-notifier activate параметр
Получение Bundle IDosascript динамический запрос
Пользовательский звукnode-notifier sound параметр

Полный процесс поддержки:

mermaid
graph LR
    A[Обнаружить терминал] --> B[Получить Bundle ID]
    B --> C[Кэшировать TerminalInfo]
    C --> D[Триггер события]
    D --> E{Терминал сфокусирован?}
    E -->|Да| F[🚫 Подавить уведомление]
    E -->|Нет| G[Отправить уведомление]
    G --> H{Пользователь нажал?}
    H -->|Да| I[Активировать окно терминала]
    H -->|Нет| J[Уведомление автоматически исчезает]

Windows

ФункцияПоддержкаСпособ реализации
Системное уведомлениеnode-notifier (SnoreToast)
Обнаружение терминалабиблиотека detect-terminal
Обнаружение фокусаОграничения системы
Нажатие для фокусировкиОграничения системы
Получение Bundle IDВ Windows нет такого понятия
Пользовательский звукИспользуется системный звук по умолчанию

Упрощенный процесс:

mermaid
graph LR
    A[Обнаружить терминал] --> B[Кэшировать TerminalInfo]
    B --> C[Триггер события]
    C --> D{Время тишины?}
    D -->|Да| E[🚫 Подавить уведомление]
    D -->|Нет| F[Отправить уведомление]
    F --> G[Уведомление автоматически исчезает]

Linux

ФункцияПоддержкаСпособ реализации
Системное уведомлениеnode-notifier (notify-send)
Обнаружение терминалабиблиотека detect-terminal
Обнаружение фокусаОграничения системы
Нажатие для фокусировкиОграничения системы
Получение Bundle IDВ Linux нет такого понятия
Пользовательский звукИспользуется звук окружения рабочего стола по умолчанию

Процесс аналогичен Windows, только для уведомлений используется notify-send.

Обработка сбоев обнаружения

Логика исходного кода

Когда обнаружение не удалось, detectTerminalInfo() возвращает пустой объект:

typescript
if (!terminalName) {
    return { name: null, bundleId: null, processName: null }
}

Влияние на функциональность уведомлений

ФункцияПоведение после сбоя обнаружения
Системное уведомлениеРаботает нормально (не зависит от обнаружения терминала)
Обнаружение фокусаНедоступно (isTerminalFocused() сразу возвращает false)
Нажатие для фокусировкиНедоступно (bundleId равен null, параметр activate не устанавливается)
Время тишиныРаботает нормально (независимо от обнаружения терминала)
Проверка родительской сессииРаботает нормально (независимо от обнаружения терминала)

Пример кода:

typescript
// src/notify.ts:166-175
async function isTerminalFocused(terminalInfo: TerminalInfo): Promise<boolean> {
    if (!terminalInfo.processName) return false  // ← Сразу возвращает false при сбое обнаружения
    if (process.platform !== "darwin") return false
    // ...
}

// src/notify.ts:238-240
if (process.platform === "darwin" && terminalInfo.bundleId) {
    notifyOptions.activate = terminalInfo.bundleId  // ← Не устанавливается, если bundleId null
}

Как проверить статус обнаружения?

Временный метод отладки (требуется изменение исходного кода):

typescript
// Добавить в экспорт плагина notify.ts
export const NotifyPlugin: Plugin = async (ctx) => {
    const { client } = ctx
    const config = await loadConfig()
    const terminalInfo = await detectTerminalInfo(config)

    // 👇 Добавить лог отладки
    console.log("Информация о терминале:", JSON.stringify(terminalInfo, null, 2))

    return {
        // ...
    }
}

Пример нормального вывода:

json
{
  "name": "ghostty",
  "bundleId": "com.mitchellh.ghostty",
  "processName": "Ghostty"
}

Пример сбоя обнаружения:

json
{
  "name": null,
  "bundleId": null,
  "processName": null
}

Особые сценарии: терминал tmux

Особенности tmux

tmux — это мультиплексор терминала (terminal multiplexer), который позволяет создавать несколько сессий и окон в одном окне терминала.

Способ обнаружения:

typescript
// Библиотека detect-terminal обнаруживает tmux через переменные окружения
if (process.env.TMUX) return 'tmux'

Влияние рабочего процесса tmux:

В terminals/index.md упоминается, что в рабочем процессе tmux не выполняется обнаружение фокуса. Это потому что:

  1. Сценарии с несколькими окнами: tmux может работать в нескольких окнах терминала
  2. Неоднозначная семантика фокуса: невозможно определить, на какую панель tmux смотрит пользователь
  3. Соображения пользовательского опыта: избежать подавления важных уведомлений

Доказательство в исходном коде (функция handleQuestionAsked):

typescript
// src/notify.ts:340-341
// Защита: только время тишины (без проверки фокуса для вопросов — рабочий процесс tmux)
if (isQuietHours(config)) return

Обратите внимание: для событий типа question в исходном коде явно указано "no focus check for questions - tmux workflow".

Лучшие практики ручной конфигурации типа терминала

Выбор подходящего имени терминала

Принцип: используйте стандартные имена, распознаваемые библиотекой detect-terminal.

Справочная таблица частых случаев:

Ваш терминалЗначение конфигурацииРезультат обнаружения (detect-terminal)
Ghostty"ghostty"
iTerm2"iterm2" или "iterm"
Kitty"kitty"
WezTerm"wezterm"
Alacritty"alacritty"
macOS Terminal.app"terminal" или "apple_terminal"
Hyper"hyper"
Warp"warp"
VS Code Stable"vscode"
VS Code Insiders"vscode-insiders"
Windows Terminal"windows-terminal" или "Windows Terminal"⚠️ Может потребоваться попробовать оба

Проверка, что конфигурация работает

Метод 1: Проверка журналов

Если включено ведение журнала отладки (см. выше), вы должны увидеть:

json
{
  "name": "ghostty",  // ← Должно быть имя терминала, которое вы настроили
  "bundleId": "com.mitchellh.ghostty",
  "processName": "Ghostty"
}

Метод 2: Функциональное тестирование

  1. После настройки типа терминала перезапустите OpenCode
  2. Запустите задачу AI
  3. Переключитесь на другое окно (чтобы терминал потерял фокус)
  4. Дождитесь завершения задачи

Вы должны увидеть: получено уведомление, и нажатие на уведомление (macOS) может фокусировать окно терминала.

Предостережения

Частая проблема 1: После настройки обнаружение по-прежнему не работает

Проявление: Установлено "terminal": "ghostty", но функция уведомлений работает некорректно.

Шаги по диагностике:

  1. Проверка формата JSON:
bash
cat ~/.config/opencode/kdco-notify.json | jq .
  1. Проверка написания имени терминала:
  • Убедитесь, что используется нижний регистр (например, "ghostty", а не "Ghostty")
  • Убедитесь, что это имя поддерживается detect-terminal
  1. Проверка, перезапущен ли OpenCode:

Изменения в файле конфигурации вступают в силу только после перезапуска OpenCode.

Частая проблема 2: Обнаружение фокуса macOS неточное

Проявление: Терминал на переднем плане, но всплывает уведомление, или терминал в фоне, но уведомление не всплывает.

Возможные причины:

  1. Имя процесса не совпадает:

Проверьте, есть ли имя процесса вашего терминала в таблице соответствия:

typescript
const TERMINAL_PROCESS_NAMES: Record<string, string> = {
    // ... проверьте, есть ли ваш терминал в списке
}
  1. Сбой выполнения osascript:

Протестируйте вручную в терминале:

bash
osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'

Должно вернуть имя текущего активного приложения (например, Ghostty).

  1. Чувствительность к регистру:

В исходном коде используется сравнение без учета регистра:

typescript
return frontmost.toLowerCase() === terminalInfo.processName.toLowerCase()

Убедитесь, что имя процесса в таблице соответствия соответствует фактическому имени процесса с точки зрения регистра.

Частая проблема 3: Ручная конфигурация в Windows/Linux не работает

Проявление: Конфигурация "terminal": "xxx" в Windows/Linux, но обнаружение по-прежнему не работает.

Пояснение:

Обнаружение терминала в Windows/Linux зависит от библиотеки detect-terminal, функция переопределения конфигурации в исходном коде реализована кросс-платформенно. Если конфигурация не работает, возможные причины:

  1. Неправильное имя терминала: убедитесь, что используется имя, поддерживаемое detect-terminal
  2. Терминал не в списке поддержки: см. полный список detect-terminal

Примечание: Windows/Linux не поддерживает обнаружение фокуса и нажатие для фокусировки, даже если конфигурация правильная, эти функции не будут включены.

Частая проблема 4: Ошибка обнаружения во вложенных сценариях терминала

Сценарий: Использование Alacritty в tmux, хочется распознать как tmux.

Проявление: Автоматическое обнаружение распознает как "alacritty", но вы хотите "tmux".

Решение:

Вручную настройте "terminal": "tmux":

json
{
  "terminal": "tmux"
}

Примечание: После такой конфигурации плагин будет считать, что вы используете tmux, и не будет выполнять обнаружение фокуса (в соответствии с рабочим процессом tmux).

Резюме урока

Обнаружение терминала — это основа функции интеллектуальной фильтрации opencode-notify:

  1. Процесс обнаружения: автоматическое распознавание терминала при запуске через библиотеку detect-terminal, кэширование результата
  2. Специальная обработка macOS:
    • Таблица соответствия имен процессов (TERMINAL_PROCESS_NAMES)
    • Динамическое получение Bundle ID (через osascript)
    • Обнаружение фокуса (запрос активного процесса приложения)
  3. Переопределение конфигурации: ручное указание типа терминала имеет приоритет над автоматическим обнаружением
  4. Различия платформ:
    • macOS: полная функциональность (уведомления + обнаружение фокуса + нажатие для фокусировки)
    • Windows/Linux: базовая функция уведомлений
  5. Обработка сбоев: при сбое обнаружения отправка уведомлений не блокируется, просто отключается интеллектуальная фильтрация
  6. Особые сценарии: рабочий процесс tmux не поддерживает обнаружение фокуса, чтобы избежать подавления важных уведомлений

Ключевые позиции исходного кода:

  • detectTerminalInfo(): основная функция обнаружения терминала (строки 145-164)
  • TERMINAL_PROCESS_NAMES: таблица соответствия имен процессов macOS (строки 71-84)
  • getBundleId(): динамическое получение Bundle ID (строки 135-137)
  • isTerminalFocused(): реализация обнаружения фокуса (строки 166-175)

Анонс следующего урока

В следующем уроке мы изучим Расширенное использование.

Вы узнаете:

  • Навыки и лучшие практики конфигурации
  • Настройка множественных терминалов
  • Рекомендации по оптимизации производительности
  • Совместное использование с другими плагинами OpenCode

Приложение: Справочник по исходному коду

Нажмите, чтобы развернуть позиции исходного кода

Время обновления: 2026-01-27

ФункцияПуть файлаНомера строк
Основная функция обнаружения терминалаsrc/notify.ts145-164
Таблица соответствия имен процессов macOSsrc/notify.ts71-84
Получение Bundle ID macOSsrc/notify.ts135-137
Обнаружение активного приложения macOSsrc/notify.ts139-143
Обнаружение фокуса macOSsrc/notify.ts166-175
Инкапсуляция выполнения osascriptsrc/notify.ts120-133
Определение интерфейса конфигурацииsrc/notify.ts30-54
Обнаружение фокуса в обработке завершения задачиsrc/notify.ts265
Обнаружение фокуса в обработке уведомления об ошибкеsrc/notify.ts303
Обнаружение фокуса в обработке запроса разрешенияsrc/notify.ts326
Настройка нажатия для фокусировки macOSsrc/notify.ts238-240
Обнаружение терминала при запуске плагинаsrc/notify.ts364

Ключевые константы:

  • TERMINAL_PROCESS_NAMES: Таблица соответствия имен терминалов именам процессов macOS (строки 71-84)
    • ghostty: "Ghostty"
    • kitty: "kitty"
    • iterm: "iTerm2" / iterm2: "iTerm2"
    • wezterm: "WezTerm"
    • alacritty: "Alacritty"
    • terminal: "Terminal" / apple_terminal: "Terminal"
    • hyper: "Hyper"
    • warp: "Warp"
    • vscode: "Code" / "vscode-insiders": "Code - Insiders"

Ключевые функции:

  • detectTerminalInfo(config: NotifyConfig): Promise<TerminalInfo>: Основная функция обнаружения терминала (строки 145-164)

    • Приоритет использования переопределения конфигурации (config.terminal)
    • Вызов библиотеки detect-terminal для автоматического обнаружения
    • Поиск в таблице соответствия имен процессов
    • Динамическое получение Bundle ID (macOS)
    • Возврат полного объекта информации о терминале
  • isTerminalFocused(terminalInfo: TerminalInfo): Promise<boolean>: Обнаружение, сфокусирован ли терминал (строки 166-175)

    • Проверка платформы (только macOS)
    • Проверка существования processName
    • Получение текущего активного приложения (osascript)
    • Сравнение имен процессов без учета регистра
    • Возврат true, если терминал на переднем плане
  • getBundleId(appName: string): Promise<string | null>: Динамическое получение Bundle ID приложения macOS (строки 135-137)

    • Использование osascript для запроса идентификатора приложения
    • Возврат в формате "com.mitchellh.ghostty"
  • getFrontmostApp(): Promise<string | null>: Получение имени активного приложения macOS (строки 139-143)

    • Использование osascript для запроса System Events
    • Возврат имени процесса активного приложения
  • runOsascript(script: string): Promise<string | null>: Выполнение команд AppleScript (строки 120-133)

    • Проверка платформы (только macOS)
    • Использование Bun.spawn для выполнения osascript
    • Захват и возврат вывода
    • Обработка исключений, возврат null

Бизнес-правила:

  • BR-2-1: Использование библиотеки detect-terminal для распознавания 37+ терминалов (notify.ts:147)
  • BR-2-2: Таблица соответствия имен терминалов именам процессов macOS (notify.ts:71-84)
  • BR-2-3: Динамическое получение Bundle ID, без хардкода (notify.ts:135-137)
  • BR-2-4: Обнаружение сессии tmux через переменную окружения TMUX (реализация в библиотеке)
  • BR-1-2: Подавление уведомления, когда терминал сфокусирован (notify.ts:265)
  • BR-1-6: macOS поддерживает нажатие на уведомление для фокусировки терминала (notify.ts:238-240)

Внешние зависимости:

  • detect-terminal: Библиотека обнаружения терминала, поддерживает 37+ эмуляторов терминала
  • node-notifier: Кроссплатформенная библиотека системных уведомлений
  • macOS osascript: Системный инструмент командной строки для выполнения AppleScript