Skip to content

Commit 1d71e4d

Browse files
abrichrclaude
andcommitted
feat: add /verbose and /quiet notification toggle for Telegram bot
Adds per-chat notification mode so users can switch between verbose (all events) and quiet (milestones only) modes. In quiet mode, noisy events like edits, test runs, and clones are suppressed entirely, while non-loud events are delivered silently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4441526 commit 1d71e4d

1 file changed

Lines changed: 50 additions & 4 deletions

File tree

apps/bot/src/index.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ if (!GITHUB_TOKEN) {
3939

4040
const bot = new Bot(BOT_TOKEN)
4141

42+
// ---------------------------------------------------------------------------
43+
// Notification mode: 'verbose' (default) or 'quiet', keyed by chat ID
44+
// ---------------------------------------------------------------------------
45+
46+
const chatNotifyMode = new Map<number, 'verbose' | 'quiet'>()
47+
48+
/** Events that are skipped entirely in quiet mode. */
49+
const QUIET_EVENTS = new Set(['edit', 'test_run', 'cloned'])
50+
51+
/** Events that always deliver with notification sound regardless of mode. */
52+
const LOUD_EVENTS = new Set(['pr_created', 'completed'])
53+
4254
// ---------------------------------------------------------------------------
4355
// Authorization middleware — restrict to known Telegram users
4456
// ---------------------------------------------------------------------------
@@ -176,13 +188,31 @@ bot.command('start', async (ctx: Context) => {
176188
'/task &lt;repo_url&gt; &lt;description&gt; -- Submit a dev task',
177189
'/status &lt;job_id&gt; -- Check job status',
178190
'/cancel &lt;job_id&gt; -- Cancel a running job',
191+
'/verbose -- Enable all event notifications (default)',
192+
'/quiet -- Only send milestone notifications; silence noisy events',
179193
'',
180194
'When a PR is ready, I will send approve/reject buttons.',
181195
].join('\n'),
182196
{ parse_mode: 'HTML' },
183197
)
184198
})
185199

200+
bot.command('verbose', async (ctx: Context) => {
201+
const chatId = ctx.chat!.id
202+
chatNotifyMode.set(chatId, 'verbose')
203+
await ctx.reply('\u{1F50A} Verbose mode enabled. You will receive all event notifications.')
204+
})
205+
206+
bot.command('quiet', async (ctx: Context) => {
207+
const chatId = ctx.chat!.id
208+
chatNotifyMode.set(chatId, 'quiet')
209+
await ctx.reply(
210+
'\u{1F514} Quiet mode enabled. '
211+
+ 'Only milestone notifications (loop start, test results, PR created, completed, errors) will be sent. '
212+
+ 'Noisy events (edits, test runs, cloned) are silenced.',
213+
)
214+
})
215+
186216
bot.command('task', async (ctx: Context) => {
187217
const text = ctx.message?.text ?? ''
188218
// Parse: /task <repo_url> <description...>
@@ -431,17 +461,28 @@ function startRealtimeBridge(): void {
431461
const job = await getJob(event.job_id)
432462
if (!job?.telegram_chat_id) return
433463

464+
const chatId = job.telegram_chat_id
465+
const mode = chatNotifyMode.get(chatId) ?? 'verbose'
466+
467+
// In quiet mode, skip noisy events entirely
468+
if (mode === 'quiet' && QUIET_EVENTS.has(event.event_type)) return
469+
434470
const text = `<b>[${job.id.slice(0, 8)}]</b> ${formatJobEvent(event)}`
435471

472+
// In quiet mode, deliver silently unless this is a loud event
473+
const disableNotification = mode === 'quiet' && !LOUD_EVENTS.has(event.event_type)
474+
436475
// PR created events get the approval keyboard
437476
if (event.event_type === 'pr_created' && job.pr_url) {
438-
await bot.api.sendMessage(job.telegram_chat_id, text, {
477+
await bot.api.sendMessage(chatId, text, {
439478
parse_mode: 'HTML',
440479
reply_markup: buildPrKeyboard(job.id, job.pr_url),
480+
disable_notification: disableNotification,
441481
})
442482
} else {
443-
await bot.api.sendMessage(job.telegram_chat_id, text, {
483+
await bot.api.sendMessage(chatId, text, {
444484
parse_mode: 'HTML',
485+
disable_notification: disableNotification,
445486
})
446487
}
447488
} catch (err) {
@@ -458,16 +499,21 @@ function startRealtimeBridge(): void {
458499
if (!terminal.includes(job.status)) return
459500

460501
try {
502+
const chatId = job.telegram_chat_id
503+
const mode = chatNotifyMode.get(chatId) ?? 'verbose'
461504
const text = formatJobStatus(job)
462505

463506
if (job.pr_url && job.status === JOB_STATUS.SUCCEEDED) {
464-
await bot.api.sendMessage(job.telegram_chat_id, text, {
507+
await bot.api.sendMessage(chatId, text, {
465508
parse_mode: 'HTML',
466509
reply_markup: buildPrKeyboard(job.id, job.pr_url),
467510
})
468511
} else {
469-
await bot.api.sendMessage(job.telegram_chat_id, text, {
512+
// Terminal failure: respect quiet mode (silent delivery)
513+
const disableNotification = mode === 'quiet' && job.status === JOB_STATUS.FAILED
514+
await bot.api.sendMessage(chatId, text, {
470515
parse_mode: 'HTML',
516+
disable_notification: disableNotification,
471517
})
472518
}
473519
} catch (err) {

0 commit comments

Comments
 (0)