Skip to content

Commit 8413d0d

Browse files
authored
Make notification title marker configurable (#457)
1 parent 1f2f867 commit 8413d0d

8 files changed

Lines changed: 106 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ This means you can:
369369
- **notification_all_screens** (boolean, default: `true`): Show overlay notifications on all screens (`true`) or only the main screen (`false`). Themed overlays (`glass`, `jarvis`, `sakura`) previously only showed on one screen — existing configs with those themes are migrated to `false` automatically. macOS only.
370370
- **`CLAUDE_SESSION_NAME` env var**: Set before launching `claude` to give a session a custom name. Shows in both desktop notification titles and terminal tab titles. Priority over all config-based naming. Example: `CLAUDE_SESSION_NAME="Auth Refactor" claude` or `export CLAUDE_SESSION_NAME="Feature: Auth"` then `claude`. Each terminal gets its own title automatically since peon-ping runs as a child of that Claude instance.
371371
- **notification_title_override** (string, default: `""`): Override the project name shown in notification titles. When empty, the project name is auto-detected from `/peon-ping-rename` > `CLAUDE_SESSION_NAME` > `.peon-label` > `notification_title_script` > `project_name_map` > git repo name > folder name.
372+
- **notification_title_marker** (string, default: `"●"`): Character(s) shown before the project name in notification titles and terminal tab titles. Set to `""` to disable. Example: `"🔔"`.
372373
- **notification_title_script** (string, default: `""`): Shell command run at event time to compute the project name dynamically. Receives env vars: `PEON_SESSION_ID`, `PEON_CWD`, `PEON_HOOK_EVENT`, `PEON_SESSION_NAME`. Use stdout (trimmed, max 50 chars); non-zero exit falls through to the next tier. Example: `"basename $PEON_CWD"`.
373374
- **project_name_map** (object, default: `{}`): Map directory paths to custom project labels for notifications. Keys are path patterns, values are display names. Example: `{ "/home/user/work/client-a": "Client A" }`.
374375
- **notification_templates** (object, default: `{}`): Custom message format strings for notification events. Keys are event types (`stop`, `permission`, `error`, `idle`, `question`), values are template strings with variable substitution. Available variables: `{project}`, `{summary}`, `{tool_name}`, `{status}`, `{event}`. Example: `{ "stop": "{project}: {summary}", "permission": "{project}: {tool_name} needs approval" }`.

README_ja.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ peon-ping には3つの独立したコントロールがあり、自由に組み
358358
- **notification_dismiss_seconds**(数値、デフォルト: `4`): N 秒後にオーバーレイ通知を自動的に消去。`0` に設定するとクリックで消去するまで永続的に表示。
359359
- **`CLAUDE_SESSION_NAME` 環境変数**: `claude` 起動前に設定すると、セッションにカスタム名を付けられます。デスクトップ通知タイトルとターミナルタブタイトルの両方に表示されます。すべての設定ベースの命名より優先。例: `CLAUDE_SESSION_NAME="Auth Refactor" claude` または `export CLAUDE_SESSION_NAME="Feature: Auth"` してから `claude`。各ターミナルは自動的に独自のタイトルを取得します。
360360
- **notification_title_override**(文字列、デフォルト: `""`): 通知タイトルに表示されるプロジェクト名を上書き。空の場合は自動検出: `/peon-ping-rename` > `CLAUDE_SESSION_NAME` > `.peon-label` > `notification_title_script` > `project_name_map` > git リポジトリ名 > フォルダ名。
361+
- **notification_title_marker**(文字列、デフォルト: `"●"`): 通知タイトルとターミナルタブタイトル前に表示される文字。`""` に設定すると無効化。例如:`"🔔"`
361362
- **notification_title_script**(文字列、デフォルト: `""`): イベント発生時に実行されるシェルコマンドで、プロジェクト名を動的に計算。利用可能な環境変数: `PEON_SESSION_ID``PEON_CWD``PEON_HOOK_EVENT``PEON_SESSION_NAME`。stdout を使用(トリミング済み、最大50文字); 非ゼロ終了は次のティアにフォールスルー。例: `"basename $PEON_CWD"`
362363
- **project_name_map**(オブジェクト、デフォルト: `{}`): ディレクトリパスを通知のカスタムプロジェクトラベルにマッピング。キーはパスパターン、値は表示名。例: `{ "/home/user/work/client-a": "Client A" }`
363364
- **notification_templates**(オブジェクト、デフォルト: `{}`): 通知イベントのカスタムメッセージフォーマット文字列。キーはイベントタイプ(`stop``permission``error``idle``question`)、値は変数置換付きのテンプレート文字列。利用可能な変数: `{project}``{summary}``{tool_name}``{status}``{event}`。例: `{ "stop": "{project}: {summary}", "permission": "{project}: {tool_name} needs approval" }`

README_zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ peon-ping 有三个独立的控制开关,可以混合使用:
361361
- **notification_dismiss_seconds**(数字,默认:`4`):覆盖通知在 N 秒后自动消失。设为 `0` 则通知持续显示,需点击关闭。
362362
- **`CLAUDE_SESSION_NAME` 环境变量**:在启动 `claude` 前设置,为会话指定自定义名称。同时显示在桌面通知标题和终端标签标题中,优先级高于所有配置项。示例:`CLAUDE_SESSION_NAME="Auth Refactor" claude` 或先 `export CLAUDE_SESSION_NAME="功能: Auth"``claude`
363363
- **notification_title_override**(字符串,默认:`""`):覆盖通知标题中显示的项目名称。为空时自动检测:`/peon-ping-rename` > `CLAUDE_SESSION_NAME` > `.peon-label` > `notification_title_script` > `project_name_map` > git 仓库名 > 文件夹名。
364+
- **notification_title_marker**(字符串,默认:`"●"`):通知标题和终端标签标题前显示的字符。设为 `""` 可禁用。例如:`"🔔"`
364365
- **notification_title_script**(字符串,默认:`""`):在事件触发时运行的 Shell 命令,用于动态计算项目名称。可用环境变量:`PEON_SESSION_ID``PEON_CWD``PEON_HOOK_EVENT``PEON_SESSION_NAME`。使用标准输出(去除首尾空白,最多 50 字符);非零退出码则继续查找下一层级。示例:`"basename $PEON_CWD"`
365366
- **project_name_map**(对象,默认:`{}`):将目录路径映射为通知中的自定义项目标签。键为路径模式,值为显示名称。
366367
- **notification_templates**(对象,默认:`{}`):自定义通知事件的消息格式。键为事件类型(`stop``permission``error``idle``question`),值为支持变量替换的模板字符串。可用变量:`{project}``{summary}``{tool_name}``{status}``{event}`

config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"notification_dismiss_seconds": 4,
3535
"notification_all_screens": true,
3636
"notification_title_override": "",
37+
"notification_title_marker": "",
3738
"notification_title_script": "",
3839
"project_name_map": {},
3940
"debug": false,

docs/public/llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Full list: https://openpeon.com/packs
105105

106106
Config file: `~/.claude/hooks/peon-ping/config.json`
107107

108-
Key settings: volume, categories (per-event toggles), default_pack (replaces active_pack), path_rules (glob-based per-repo pack assignment), pack_rotation, pack_rotation_mode, desktop_notifications, silent_window_seconds, session_start_cooldown_seconds (dedup greeting sounds when many workspaces open at once; default 30s), headphones_only (mute when no headphones detected), suppress_sound_when_tab_focused (skip sound when the generating tab is focused), notification_position (screen position for overlays), notification_dismiss_seconds (auto-dismiss timer, 0 = persistent), notification_title_override (custom project label), notification_title_script (shell command for dynamic project name; receives PEON_SESSION_ID/PEON_CWD/PEON_HOOK_EVENT/PEON_SESSION_NAME env vars), project_name_map (directory-to-label mapping), notification_templates (per-event message format strings with {project}/{summary}/{tool_name} variables), debug (enable structured hook logging), debug_retention_days (auto-prune old logs, default 7).
108+
Key settings: volume, categories (per-event toggles), default_pack (replaces active_pack), path_rules (glob-based per-repo pack assignment), pack_rotation, pack_rotation_mode, desktop_notifications, silent_window_seconds, session_start_cooldown_seconds (dedup greeting sounds when many workspaces open at once; default 30s), headphones_only (mute when no headphones detected), suppress_sound_when_tab_focused (skip sound when the generating tab is focused), notification_position (screen position for overlays), notification_dismiss_seconds (auto-dismiss timer, 0 = persistent), notification_title_override (custom project label), notification_title_marker (character(s) before project name in titles, set to "" to disable), notification_title_script (shell command for dynamic project name; receives PEON_SESSION_ID/PEON_CWD/PEON_HOOK_EVENT/PEON_SESSION_NAME env vars), project_name_map (directory-to-label mapping), notification_templates (per-event message format strings with {project}/{summary}/{tool_name} variables), debug (enable structured hook logging), debug_retention_days (auto-prune old logs, default 7).
109109

110110
Pack selection hierarchy (highest to lowest priority): session_override (per-session via /peon-ping-use) > path_rules (glob match on working directory) > pack_rotation (random/round-robin from list) > default_pack (static fallback) > hardcoded "peon". Invalid or missing packs fall through to the next layer.
111111

install.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,8 +1861,8 @@ try {
18611861
$config = Get-PeonConfigRaw $ConfigPath | ConvertFrom-Json
18621862
} catch {
18631863
$_configError = "$_"
1864-
# Fall back to minimal defaults so the hook can still run (logging requires PEON_DEBUG=1 when config is broken)
1865-
$config = [PSCustomObject]@{ enabled = $true; debug = $false; volume = 0.5; debug_retention_days = 7 }
1864+
# Fall back to minimal defaults so the hook can still run (logging requires PEON_DEBUG=1 when config is broken)
1865+
$config = [PSCustomObject]@{ enabled = $true; debug = $false; volume = 0.5; debug_retention_days = 7; notification_title_marker = '●' }
18661866
}
18671867
18681868
# NOTE: enabled check moved below logging init so paused invocations are visible in debug logs
@@ -2686,7 +2686,7 @@ if ($null -eq $desktopNotif) { $desktopNotif = $true }
26862686
if ($notify -and $desktopNotif) {
26872687
$winNotifyScript = Join-Path $InstallDir "scripts\win-notify.ps1"
26882688
if (Test-Path $winNotifyScript) {
2689-
$marker = [char]0x25CF # ●
2689+
$marker = if ($config.notification_title_marker) { $config.notification_title_marker } else { [char]0x25CF }
26902690
$notifTitle = "$marker $project`: $notifyStatus"
26912691
$dismissSecs = if ($config.notification_dismiss_seconds) { $config.notification_dismiss_seconds } else { 4 }
26922692
# Resolve parent PID (the IDE/terminal that spawned Claude Code) for click-to-focus

peon.sh

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,9 @@ if verbose:
10951095
print('peon-ping: label override: ' + _lbl)
10961096
if _pmap:
10971097
print('peon-ping: project name map: ' + str(len(_pmap)) + ' pattern(s)')
1098+
_mrk = c.get('notification_title_marker', '●')
1099+
if _mrk != '●':
1100+
print('peon-ping: title marker: ' + (_mrk if _mrk else '(disabled)'))
10981101
_tpls = c.get('notification_templates', {})
10991102
if _tpls:
11001103
print('peon-ping: notification templates:')
@@ -1433,10 +1436,48 @@ except Exception:
14331436
cfg = {}
14341437
cfg['notification_title_override'] = label
14351438
json.dump(cfg, open(config_path, 'w'), indent=2)
1436-
print(f'peon-ping: label override set to \"{label}\"')
1439+
print(f'peon-ping: label override set to "{label}"')
14371440
" "$LABEL_ARG"
14381441
_rc=$?; [ "$_rc" -ne 0 ] && exit "$_rc"
14391442
sync_adapter_configs; exit 0 ;;
1443+
marker)
1444+
MARKER_ARG="${3:-}"
1445+
if [ -z "$MARKER_ARG" ]; then
1446+
python3 -c "
1447+
import json
1448+
try:
1449+
cfg = json.load(open('$CONFIG_PY'))
1450+
m = cfg.get('notification_title_marker', '●')
1451+
if m == '●':
1452+
print('peon-ping: title marker: ● (default)')
1453+
elif m:
1454+
print(f'peon-ping: title marker: {m}')
1455+
else:
1456+
print('peon-ping: title marker: (disabled)')
1457+
except Exception:
1458+
print('peon-ping: title marker: ● (default)')
1459+
"
1460+
exit 0
1461+
fi
1462+
python3 -c "
1463+
import json, sys
1464+
config_path = '$GLOBAL_CONFIG_PY'
1465+
marker = sys.argv[1]
1466+
try:
1467+
cfg = json.load(open(config_path))
1468+
except Exception:
1469+
cfg = {}
1470+
cfg['notification_title_marker'] = marker
1471+
json.dump(cfg, open(config_path, 'w'), indent=2)
1472+
if marker == '●':
1473+
print('peon-ping: title marker reset to default ●')
1474+
elif marker:
1475+
print(f'peon-ping: title marker set to \"{marker}\"')
1476+
else:
1477+
print('peon-ping: title marker disabled')
1478+
" "$MARKER_ARG"
1479+
_rc=$?; [ "$_rc" -ne 0 ] && exit "$_rc"
1480+
sync_adapter_configs; exit 0 ;;
14401481
test)
14411482
# Read config to check if notifications are enabled and get style
14421483
_py_out="$(python3 -c "
@@ -2828,6 +2869,10 @@ if 'notification_all_screens' not in cfg:
28282869
cfg['notification_all_screens'] = _theme not in ('glass', 'jarvis', 'sakura')
28292870
changed = True
28302871
migrations.append('notification_all_screens')
2872+
if 'notification_title_marker' not in cfg:
2873+
cfg['notification_title_marker'] = '●'
2874+
changed = True
2875+
migrations.append('notification_title_marker')
28312876
if changed:
28322877
json.dump(cfg, open(config_path, 'w'), indent=2)
28332878
print('peon-ping: config keys updated (' + ', '.join(migrations) + ')')
@@ -4284,6 +4329,7 @@ print('NOTIF_STYLE=' + q(cfg.get('notification_style', 'overlay')))
42844329
print('NOTIF_POSITION=' + q(cfg.get('notification_position', 'top-center')))
42854330
print('NOTIF_DISMISS=' + q(str(cfg.get('notification_dismiss_seconds', 4))))
42864331
print('NOTIF_ALL_SCREENS=' + ('true' if cfg.get('notification_all_screens', True) else 'false'))
4332+
print('NOTIF_MARKER=' + q(cfg.get('notification_title_marker', '●')))
42874333
print('USE_SOUND_EFFECTS_DEVICE=' + q(str(use_sound_effects_device).lower()))
42884334
print('LINUX_AUDIO_PLAYER=' + q(linux_audio_player))
42894335
print('PEON_SSH_AUDIO_MODE=' + q(str(cfg.get('ssh_audio_mode', 'relay'))))
@@ -4363,7 +4409,7 @@ if [ "${PEON_EXIT:-true}" = "true" ]; then
43634409
# Maintain tab title even on suppressed events (plan mode, unknown events, subagent start).
43644410
# PROJECT is only emitted by paths that should maintain the title; agent/disabled paths omit it.
43654411
if [ -n "${PROJECT:-}" ] && [ "${EVENT:-}" != "SessionEnd" ]; then
4366-
{ printf '\033]0;%s\007' "${MARKER:-}${PROJECT}: ${STATUS:-working}" > /dev/tty; } 2>/dev/null || true
4412+
{ printf '\033]0;%s\007' "${NOTIF_MARKER-${MARKER}}${PROJECT}: ${STATUS:-working}" > /dev/tty; } 2>/dev/null || true
43674413
fi
43684414
exit 0
43694415
fi
@@ -4485,7 +4531,7 @@ if [ "$EVENT" = "SessionStart" ] && { [ "$PEON_PLATFORM" = "devcontainer" ] || [
44854531
fi
44864532

44874533
# --- Build tab title ---
4488-
TITLE="${MARKER}${PROJECT}: ${STATUS}"
4534+
TITLE="${NOTIF_MARKER-${MARKER}}${PROJECT}: ${STATUS}"
44894535

44904536
# --- Resolve TTY for escape sequences ---
44914537
# Write to /dev/tty so the escape sequence reaches the terminal directly.

tests/peon.bats

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,55 @@ json.dump(c, open('$TEST_DIR/config.json', 'w'), indent=2)
976976
[ "$val" = "True" ]
977977
}
978978

979+
@test "notifications marker shows default" {
980+
run bash "$PEON_SH" notifications marker
981+
[ "$status" -eq 0 ]
982+
[[ "$output" == *""* ]]
983+
}
984+
985+
@test "notifications marker set to empty disables it" {
986+
run bash "$PEON_SH" notifications marker ""
987+
[ "$status" -eq 0 ]
988+
[[ "$output" == *"disabled"* ]]
989+
val=$(/usr/bin/python3 -c "import json; print(json.load(open('$TEST_DIR/config.json')).get('notification_title_marker', '●'))")
990+
[ "$val" = "" ]
991+
}
992+
993+
@test "notifications marker set to custom" {
994+
run bash "$PEON_SH" notifications marker "🔔"
995+
[ "$status" -eq 0 ]
996+
[[ "$output" == *"🔔"* ]]
997+
val=$(/usr/bin/python3 -c "import json; print(json.load(open('$TEST_DIR/config.json')).get('notification_title_marker', '●'))")
998+
[ "$val" = "🔔" ]
999+
}
1000+
1001+
@test "notification_title_marker appears in notification title" {
1002+
/usr/bin/python3 -c "
1003+
import json
1004+
cfg = json.load(open('$TEST_DIR/config.json'))
1005+
cfg['notification_style'] = 'standard'
1006+
json.dump(cfg, open('$TEST_DIR/config.json', 'w'))
1007+
"
1008+
run_peon '{"hook_event_name":"Stop","cwd":"/tmp/myproject","session_id":"s1","permission_mode":"default"}'
1009+
[ "$PEON_EXIT" -eq 0 ]
1010+
[ -f "$TEST_DIR/terminal_notifier.log" ]
1011+
grep -q "" "$TEST_DIR/terminal_notifier.log"
1012+
}
1013+
1014+
@test "notification_title_marker empty removes marker from title" {
1015+
/usr/bin/python3 -c "
1016+
import json
1017+
cfg = json.load(open('$TEST_DIR/config.json'))
1018+
cfg['notification_style'] = 'standard'
1019+
cfg['notification_title_marker'] = ''
1020+
json.dump(cfg, open('$TEST_DIR/config.json', 'w'))
1021+
"
1022+
run_peon '{"hook_event_name":"Stop","cwd":"/tmp/myproject","session_id":"s1","permission_mode":"default"}'
1023+
[ "$PEON_EXIT" -eq 0 ]
1024+
[ -f "$TEST_DIR/terminal_notifier.log" ]
1025+
! grep -q "" "$TEST_DIR/terminal_notifier.log"
1026+
}
1027+
9791028
# ============================================================
9801029
# packs list
9811030
# ============================================================

0 commit comments

Comments
 (0)