mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix: add Telegram notification context (#768)
This commit is contained in:
+27
-5
@@ -6,9 +6,9 @@
|
||||
*/
|
||||
|
||||
import { Bot, Context, InlineKeyboard } from 'grammy'
|
||||
import { SyncEngine, Session } from '../sync/syncEngine'
|
||||
import { SyncEngine, Session, type Machine } from '../sync/syncEngine'
|
||||
import { handleCallback, CallbackContext } from './callbacks'
|
||||
import { formatSessionNotification, createNotificationKeyboard } from './sessionView'
|
||||
import { formatReadyNotification, formatSessionNotification, createNotificationKeyboard } from './sessionView'
|
||||
import { getAgentName } from '../notifications/sessionInfo'
|
||||
import type { NotificationChannel, TaskNotification } from '../notifications/notificationTypes'
|
||||
import type { Store } from '../store'
|
||||
@@ -184,6 +184,28 @@ export class HappyBot implements NotificationChannel {
|
||||
return stored?.namespace ?? null
|
||||
}
|
||||
|
||||
private getSessionMachine(session: Session): Machine | undefined {
|
||||
if (!this.syncEngine) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const machineId = session.metadata?.machineId
|
||||
if (machineId) {
|
||||
const machine = this.syncEngine.getMachineByNamespace(machineId, session.namespace)
|
||||
if (machine) {
|
||||
return machine
|
||||
}
|
||||
}
|
||||
|
||||
const host = session.metadata?.host
|
||||
if (!host) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.syncEngine.getMachinesByNamespace(session.namespace)
|
||||
.find((machine) => machine.metadata?.host === host)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification when agent is ready for input.
|
||||
*/
|
||||
@@ -192,7 +214,7 @@ export class HappyBot implements NotificationChannel {
|
||||
return
|
||||
}
|
||||
|
||||
const agentName = getAgentName(session)
|
||||
const text = formatReadyNotification(session, this.getSessionMachine(session))
|
||||
const url = buildMiniAppDeepLink(this.publicUrl, `session_${session.id}`)
|
||||
const keyboard = new InlineKeyboard()
|
||||
.webApp('Open Session', url)
|
||||
@@ -206,7 +228,7 @@ export class HappyBot implements NotificationChannel {
|
||||
try {
|
||||
await this.bot.api.sendMessage(
|
||||
chatId,
|
||||
`It's ready!\n\n${agentName} is waiting for your command`,
|
||||
text,
|
||||
{ reply_markup: keyboard }
|
||||
)
|
||||
} catch (error) {
|
||||
@@ -223,7 +245,7 @@ export class HappyBot implements NotificationChannel {
|
||||
return
|
||||
}
|
||||
|
||||
const text = formatSessionNotification(session)
|
||||
const text = formatSessionNotification(session, this.getSessionMachine(session))
|
||||
const keyboard = createNotificationKeyboard(session, this.publicUrl)
|
||||
|
||||
const chatIds = this.getBoundChatIds(session.namespace)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import type { Machine, Session } from '../sync/syncEngine'
|
||||
import { formatReadyNotification, formatSessionNotification } from './sessionView'
|
||||
|
||||
function createSession(overrides: Partial<Session> = {}): Session {
|
||||
return {
|
||||
id: 'session-1234567890',
|
||||
namespace: 'default',
|
||||
seq: 1,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
active: true,
|
||||
activeAt: 0,
|
||||
metadata: {
|
||||
path: '/home/alice/infra',
|
||||
host: 'devbox.local',
|
||||
homeDir: '/home/alice',
|
||||
name: 'rotate HAPI secrets',
|
||||
machineId: 'machine-1',
|
||||
flavor: 'codex'
|
||||
},
|
||||
metadataVersion: 1,
|
||||
agentState: null,
|
||||
agentStateVersion: 0,
|
||||
thinking: false,
|
||||
thinkingAt: 0,
|
||||
model: null,
|
||||
modelReasoningEffort: null,
|
||||
effort: null,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMachine(overrides: Partial<Machine> = {}): Machine {
|
||||
return {
|
||||
id: 'machine-1',
|
||||
namespace: 'default',
|
||||
seq: 1,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
active: true,
|
||||
activeAt: 0,
|
||||
metadata: {
|
||||
host: 'devbox.local',
|
||||
platform: 'linux',
|
||||
happyCliVersion: '0.1.0',
|
||||
displayName: 'Work Laptop'
|
||||
},
|
||||
metadataVersion: 1,
|
||||
runnerState: null,
|
||||
runnerStateVersion: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('Telegram session notifications', () => {
|
||||
it('adds session, machine, and path context to ready notifications', () => {
|
||||
expect(formatReadyNotification(createSession(), createMachine())).toBe([
|
||||
'Ready: rotate HAPI secrets on Work Laptop',
|
||||
'',
|
||||
'Codex is waiting for your command',
|
||||
'Session: rotate HAPI secrets',
|
||||
'Path: ~/infra'
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('keeps the previous ready notification text when context is unavailable', () => {
|
||||
const session = createSession({ metadata: null })
|
||||
|
||||
expect(formatReadyNotification(session)).toBe([
|
||||
"It's ready!",
|
||||
'',
|
||||
'Agent is waiting for your command'
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('adds session, machine, and path context to permission notifications without losing tool details', () => {
|
||||
const session = createSession({
|
||||
agentState: {
|
||||
requests: {
|
||||
req1: {
|
||||
tool: 'Bash',
|
||||
arguments: { command: 'bun test' },
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(formatSessionNotification(session, createMachine())).toBe([
|
||||
'Action required: rotate HAPI secrets on Work Laptop',
|
||||
'',
|
||||
'Codex requests permission',
|
||||
'Session: rotate HAPI secrets',
|
||||
'Path: ~/infra',
|
||||
'Tool: Bash',
|
||||
'Command: bun test'
|
||||
].join('\n'))
|
||||
})
|
||||
})
|
||||
@@ -6,18 +6,51 @@
|
||||
*/
|
||||
|
||||
import { InlineKeyboard } from 'grammy'
|
||||
import type { Session } from '../sync/syncEngine'
|
||||
import type { Machine, Session } from '../sync/syncEngine'
|
||||
import { ACTIONS } from './callbacks'
|
||||
import { createCallbackData, truncate, getSessionName } from './renderer'
|
||||
import { getAgentName } from '../notifications/sessionInfo'
|
||||
|
||||
const MAX_TOOL_ARGS_LENGTH = 150
|
||||
|
||||
type NotificationContext = {
|
||||
hasContext: boolean
|
||||
heading: string
|
||||
details: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a compact notification when the agent is ready for input.
|
||||
*/
|
||||
export function formatReadyNotification(session: Session, machine?: Machine): string {
|
||||
const agentName = getAgentName(session)
|
||||
const context = buildNotificationContext(session, machine)
|
||||
|
||||
if (!context.hasContext) {
|
||||
return `It's ready!\n\n${agentName} is waiting for your command`
|
||||
}
|
||||
|
||||
return [
|
||||
`Ready: ${context.heading}`,
|
||||
'',
|
||||
`${agentName} is waiting for your command`,
|
||||
...context.details
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a compact session notification for permission requests
|
||||
*/
|
||||
export function formatSessionNotification(session: Session): string {
|
||||
const name = getSessionName(session)
|
||||
const lines: string[] = ['Permission Request', '', `Session: ${name}`]
|
||||
export function formatSessionNotification(session: Session, machine?: Machine): string {
|
||||
const context = buildNotificationContext(session, machine)
|
||||
const lines: string[] = context.hasContext
|
||||
? [
|
||||
`Action required: ${context.heading}`,
|
||||
'',
|
||||
`${getAgentName(session)} requests permission`,
|
||||
...context.details
|
||||
]
|
||||
: ['Permission Request', '', `Session: ${getSessionName(session)}`]
|
||||
|
||||
const requests = session.agentState?.requests
|
||||
if (requests) {
|
||||
@@ -35,6 +68,63 @@ export function formatSessionNotification(session: Session): string {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildNotificationContext(session: Session, machine?: Machine): NotificationContext {
|
||||
const sessionName = getContextSessionName(session)
|
||||
const machineName = getMachineName(session, machine)
|
||||
const path = formatSessionPath(session)
|
||||
const heading = formatHeading(sessionName, machineName)
|
||||
const details: string[] = []
|
||||
|
||||
if (sessionName) {
|
||||
details.push(`Session: ${sessionName}`)
|
||||
}
|
||||
if (path) {
|
||||
details.push(`Path: ${path}`)
|
||||
}
|
||||
|
||||
return {
|
||||
hasContext: Boolean(heading || details.length > 0),
|
||||
heading: heading || getSessionName(session),
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
function getContextSessionName(session: Session): string | null {
|
||||
if (session.metadata?.name) return session.metadata.name
|
||||
if (session.metadata?.summary?.text) return session.metadata.summary.text
|
||||
if (session.metadata?.path) return getSessionName(session)
|
||||
return null
|
||||
}
|
||||
|
||||
function getMachineName(session: Session, machine?: Machine): string | null {
|
||||
const name = machine?.metadata?.displayName
|
||||
?? machine?.metadata?.host
|
||||
?? session.metadata?.host
|
||||
?? null
|
||||
const trimmed = name?.trim()
|
||||
return trimmed ? trimmed : null
|
||||
}
|
||||
|
||||
function formatHeading(sessionName: string | null, machineName: string | null): string {
|
||||
if (sessionName && machineName) return `${sessionName} on ${machineName}`
|
||||
if (sessionName) return sessionName
|
||||
if (machineName) return machineName
|
||||
return ''
|
||||
}
|
||||
|
||||
function formatSessionPath(session: Session): string | null {
|
||||
const path = session.metadata?.path?.trim()
|
||||
if (!path) return null
|
||||
|
||||
const homeDir = session.metadata?.homeDir?.trim()
|
||||
if (!homeDir) return path
|
||||
if (path === homeDir) return '~'
|
||||
if (path.startsWith(`${homeDir}/`)) {
|
||||
return `~/${path.slice(homeDir.length + 1)}`
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification keyboard for quick actions
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user