mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
refactor(telegram): remove interactive features and simplify to notifications only
Remove all interactive features from the Telegram bot that are now handled by the Telegram Mini App. The bot now serves a single purpose: sending notifications for permission requests and ready events. Changes: - bot.ts: Remove /help command and message handler, simplify /start to show Mini App link - callbacks.ts: Remove all callback handlers except APPROVE and DENY for permissions - sessionView.ts: Remove detail views and settings, keep only notification formatting - renderer.ts: Remove session/machine list rendering, keep utility functions This reduces the codebase by ~1000 lines (58% reduction) while preserving notification functionality that the Mini App cannot provide.
This commit is contained in:
+25
-86
@@ -1,24 +1,17 @@
|
||||
/**
|
||||
* Telegram Bot for HAPI
|
||||
*
|
||||
* Main bot class that initializes grammy, applies middleware,
|
||||
* and sets up command handlers.
|
||||
* Simplified bot that only handles notifications (permission requests and ready events).
|
||||
* All interactive features are handled by the Telegram Mini App.
|
||||
*/
|
||||
|
||||
import { Bot, Context, NextFunction, InlineKeyboard } from 'grammy'
|
||||
import { SyncEngine, SyncEvent, Session } from '../sync/syncEngine'
|
||||
import { getSessionName } from './renderer'
|
||||
import {
|
||||
handleCallback,
|
||||
CallbackContext,
|
||||
} from './callbacks'
|
||||
import {
|
||||
formatSessionNotification,
|
||||
createNotificationKeyboard
|
||||
} from './sessionView'
|
||||
import { handleCallback, CallbackContext } from './callbacks'
|
||||
import { formatSessionNotification, createNotificationKeyboard } from './sessionView'
|
||||
|
||||
export interface BotContext extends Context {
|
||||
// Extended context for future use (session state, etc.)
|
||||
// Extended context for future use
|
||||
}
|
||||
|
||||
export interface HappyBotConfig {
|
||||
@@ -29,7 +22,7 @@ export interface HappyBotConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* HAPI Telegram Bot
|
||||
* HAPI Telegram Bot - Notification-only mode
|
||||
*/
|
||||
export class HappyBot {
|
||||
private bot: Bot<BotContext>
|
||||
@@ -40,13 +33,13 @@ export class HappyBot {
|
||||
private readonly allowlistConfigured: boolean
|
||||
|
||||
// Track last known permission requests per session to detect new ones
|
||||
private lastKnownRequests: Map<string, Set<string>> = new Map() // sessionId -> requestIds
|
||||
private lastKnownRequests: Map<string, Set<string>> = new Map()
|
||||
|
||||
// Debounce timers for notifications
|
||||
private notificationDebounce: Map<string, NodeJS.Timeout> = new Map() // sessionId -> timer
|
||||
private notificationDebounce: Map<string, NodeJS.Timeout> = new Map()
|
||||
|
||||
// Track ready notifications to avoid spam
|
||||
private lastReadyNotificationAt: Map<string, number> = new Map() // sessionId -> timestamp
|
||||
private lastReadyNotificationAt: Map<string, number> = new Map()
|
||||
|
||||
// Unsubscribe function for sync events
|
||||
private unsubscribeSyncEvents: (() => void) | null = null
|
||||
@@ -60,9 +53,9 @@ export class HappyBot {
|
||||
this.bot = new Bot<BotContext>(config.botToken)
|
||||
this.setupMiddleware()
|
||||
this.setupCommands()
|
||||
|
||||
if (this.allowlistConfigured) {
|
||||
this.setupCallbacks()
|
||||
this.setupMessageHandler()
|
||||
}
|
||||
|
||||
// Subscribe to sync events immediately if engine is available
|
||||
@@ -173,6 +166,7 @@ export class HappyBot {
|
||||
* Setup command handlers
|
||||
*/
|
||||
private setupCommands(): void {
|
||||
// When allowlist is not configured, show setup instructions
|
||||
if (!this.allowlistConfigured) {
|
||||
this.bot.command('start', async (ctx) => {
|
||||
const chatId = ctx.chat?.id
|
||||
@@ -188,48 +182,25 @@ export class HappyBot {
|
||||
return
|
||||
}
|
||||
|
||||
// /start - Status + help
|
||||
this.bot.command('start', async (ctx) => {
|
||||
const sessionCount = this.syncEngine?.getActiveSessions().length ?? 0
|
||||
const machineCount = this.syncEngine?.getOnlineMachines().length ?? 0
|
||||
|
||||
await ctx.reply(
|
||||
`Welcome to HAPI Bot!\n\n` +
|
||||
`Active Sessions: ${sessionCount}\n` +
|
||||
`Online Machines: ${machineCount}\n\n` +
|
||||
`Commands:\n` +
|
||||
`/app - Open the Mini App\n` +
|
||||
`/help - Show help\n`
|
||||
)
|
||||
})
|
||||
|
||||
// /help - Show help information
|
||||
this.bot.command('help', async (ctx) => {
|
||||
await ctx.reply(
|
||||
`HAPI Bot Help\n\n` +
|
||||
`HAPI Bot is a notification layer for HAPI sessions.\n\n` +
|
||||
`Commands:\n` +
|
||||
`/start - Start the bot or show status\n` +
|
||||
`/app - Open the Mini App\n` +
|
||||
`/help - Show this help message\n\n` +
|
||||
`Use the Mini App for:\n` +
|
||||
`- Session list and full chat UI\n` +
|
||||
`- Approving/denying permissions\n` +
|
||||
`- Aborting sessions and changing modes/models\n` +
|
||||
`- Viewing machines and creating new sessions`
|
||||
)
|
||||
})
|
||||
|
||||
// /app - Open Telegram Mini App
|
||||
// /app - Open Telegram Mini App (primary entry point)
|
||||
this.bot.command('app', async (ctx) => {
|
||||
const keyboard = new InlineKeyboard().webApp('📱 Open App', this.miniAppUrl)
|
||||
const keyboard = new InlineKeyboard().webApp('Open App', this.miniAppUrl)
|
||||
await ctx.reply('Open HAPI Mini App:', { reply_markup: keyboard })
|
||||
})
|
||||
|
||||
// /start - Simple welcome with Mini App link
|
||||
this.bot.command('start', async (ctx) => {
|
||||
const keyboard = new InlineKeyboard().webApp('Open App', this.miniAppUrl)
|
||||
await ctx.reply(
|
||||
'Welcome to HAPI Bot!\n\n' +
|
||||
'Use the Mini App for full session management.',
|
||||
{ reply_markup: keyboard }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup callback query handlers (InlineKeyboard buttons)
|
||||
* Setup callback query handlers for notification buttons
|
||||
*/
|
||||
private setupCallbacks(): void {
|
||||
this.bot.on('callback_query:data', async (ctx) => {
|
||||
@@ -240,7 +211,6 @@ export class HappyBot {
|
||||
|
||||
const data = ctx.callbackQuery.data
|
||||
|
||||
// Handle other callbacks
|
||||
const callbackContext: CallbackContext = {
|
||||
syncEngine: this.syncEngine,
|
||||
answerCallback: async (text?: string) => {
|
||||
@@ -250,11 +220,6 @@ export class HappyBot {
|
||||
await ctx.editMessageText(text, {
|
||||
reply_markup: keyboard
|
||||
})
|
||||
},
|
||||
sendMessage: async (text, keyboard) => {
|
||||
await ctx.reply(text, {
|
||||
reply_markup: keyboard
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,28 +227,6 @@ export class HappyBot {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup text message handler for sending messages to Claude
|
||||
*/
|
||||
private setupMessageHandler(): void {
|
||||
// Handle text messages (non-commands)
|
||||
this.bot.on('message:text', async (ctx) => {
|
||||
// Skip if it's a command
|
||||
if (ctx.message.text.startsWith('/')) return
|
||||
|
||||
if (!this.syncEngine) {
|
||||
await ctx.reply('Not ready yet. Try again in a moment.')
|
||||
return
|
||||
}
|
||||
|
||||
const keyboard = new InlineKeyboard().webApp('📱 Open App', this.miniAppUrl)
|
||||
await ctx.reply(
|
||||
'Chat and session controls are available in the Mini App.',
|
||||
{ reply_markup: keyboard }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sync engine events for notifications
|
||||
*/
|
||||
@@ -308,10 +251,7 @@ export class HappyBot {
|
||||
this.sendReadyNotification(event.sessionId).catch((error) => {
|
||||
console.error('[HAPIBot] Failed to send ready notification:', error)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +299,6 @@ export class HappyBot {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if session has new permission requests and send notification
|
||||
*/
|
||||
@@ -422,8 +361,8 @@ export class HappyBot {
|
||||
return
|
||||
}
|
||||
|
||||
const text = formatSessionNotification(session, 'permission')
|
||||
const keyboard = createNotificationKeyboard(session)
|
||||
const text = formatSessionNotification(session)
|
||||
const keyboard = createNotificationKeyboard(session, this.miniAppUrl)
|
||||
|
||||
// Send to all allowed chat IDs
|
||||
for (const chatId of this.allowedChatIds) {
|
||||
|
||||
@@ -1,126 +1,20 @@
|
||||
/**
|
||||
* Callback Query Handlers for Telegram
|
||||
*
|
||||
* Handles InlineKeyboard button interactions for sessions, permissions, etc.
|
||||
* Handles InlineKeyboard button interactions for permission notifications.
|
||||
* Simplified to only support APPROVE and DENY actions.
|
||||
*/
|
||||
|
||||
import { InlineKeyboard } from 'grammy'
|
||||
import type { Session, Machine, SyncEngine } from '../sync/syncEngine'
|
||||
import {
|
||||
createCallbackData,
|
||||
parseCallbackData,
|
||||
findSessionByPrefix,
|
||||
findMachineByPrefix,
|
||||
formatSessionList,
|
||||
formatMachineList,
|
||||
getSessionName,
|
||||
getSessionStatusEmoji
|
||||
} from './renderer'
|
||||
import {
|
||||
formatSessionDetailView,
|
||||
createSessionDetailKeyboard as createDetailKeyboard,
|
||||
formatSettingsView,
|
||||
createSettingsKeyboard
|
||||
} from './sessionView'
|
||||
import type { Session, SyncEngine } from '../sync/syncEngine'
|
||||
import { parseCallbackData, findSessionByPrefix } from './renderer'
|
||||
|
||||
// Callback action types
|
||||
// Callback action types (simplified - only permission actions)
|
||||
export const ACTIONS = {
|
||||
// Session actions
|
||||
VIEW_SESSION: 'vs',
|
||||
REFRESH_SESSION: 'rs',
|
||||
BACK_TO_LIST: 'bl',
|
||||
|
||||
// Permission actions
|
||||
APPROVE: 'ap',
|
||||
APPROVE_EDITS: 'ae',
|
||||
APPROVE_BYPASS: 'ab',
|
||||
DENY: 'dn',
|
||||
|
||||
// Session control
|
||||
ABORT: 'at',
|
||||
SETTINGS: 'st',
|
||||
BACK_TO_SESSION: 'bs',
|
||||
|
||||
// Settings - Permission Mode
|
||||
SET_MODE_DEFAULT: 'md',
|
||||
SET_MODE_EDITS: 'me',
|
||||
SET_MODE_BYPASS: 'mb',
|
||||
SET_MODE_PLAN: 'mp',
|
||||
|
||||
// Settings - Model
|
||||
SET_MODEL_DEFAULT: 'xd',
|
||||
SET_MODEL_SONNET: 'xs',
|
||||
SET_MODEL_OPUS: 'xo',
|
||||
|
||||
// Machine actions
|
||||
VIEW_MACHINE: 'vm',
|
||||
SPAWN_SESSION: 'sp',
|
||||
|
||||
// Navigation
|
||||
REFRESH_LIST: 'rl',
|
||||
REFRESH_MACHINES: 'rm'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Create session list keyboard
|
||||
*/
|
||||
export function createSessionListKeyboard(sessions: Session[]): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
|
||||
// Add view buttons for first 5 sessions (2 per row)
|
||||
const display = sessions.slice(0, 8)
|
||||
for (let i = 0; i < display.length; i++) {
|
||||
const session = display[i]
|
||||
const name = getSessionName(session)
|
||||
const emoji = getSessionStatusEmoji(session)
|
||||
const label = `${emoji} ${name.slice(0, 15)}`
|
||||
const callback = createCallbackData(ACTIONS.VIEW_SESSION, session.id)
|
||||
|
||||
keyboard.text(label, callback)
|
||||
|
||||
// 2 buttons per row
|
||||
if (i % 2 === 1) {
|
||||
keyboard.row()
|
||||
}
|
||||
}
|
||||
|
||||
// Add refresh button
|
||||
keyboard.row()
|
||||
keyboard.text('🔄 Refresh', createCallbackData(ACTIONS.REFRESH_LIST, 'list'))
|
||||
|
||||
return keyboard
|
||||
}
|
||||
|
||||
/**
|
||||
* Create session detail keyboard
|
||||
* Re-export from sessionView for backward compatibility
|
||||
*/
|
||||
export function createSessionDetailKeyboard(session: Session): InlineKeyboard {
|
||||
return createDetailKeyboard(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create machine list keyboard
|
||||
*/
|
||||
export function createMachineListKeyboard(machines: Machine[]): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
|
||||
// Add spawn session buttons for each machine
|
||||
for (const machine of machines.slice(0, 6)) {
|
||||
const name = machine.metadata?.displayName || machine.metadata?.host || 'Unknown'
|
||||
const label = `📡 ${name.slice(0, 20)}`
|
||||
const callback = createCallbackData(ACTIONS.SPAWN_SESSION, machine.id)
|
||||
|
||||
keyboard.text(label, callback)
|
||||
keyboard.row()
|
||||
}
|
||||
|
||||
// Add refresh button
|
||||
keyboard.text('🔄 Refresh', createCallbackData(ACTIONS.REFRESH_MACHINES, 'machines'))
|
||||
|
||||
return keyboard
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback handler context
|
||||
*/
|
||||
@@ -128,27 +22,6 @@ export interface CallbackContext {
|
||||
syncEngine: SyncEngine
|
||||
answerCallback: (text?: string) => Promise<void>
|
||||
editMessage: (text: string, keyboard?: InlineKeyboard) => Promise<void>
|
||||
sendMessage: (text: string, keyboard?: InlineKeyboard) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely edit message, ignoring "message not modified" errors
|
||||
*/
|
||||
async function safeEditMessage(
|
||||
ctx: CallbackContext,
|
||||
text: string,
|
||||
keyboard?: InlineKeyboard
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ctx.editMessage(text, keyboard)
|
||||
} catch (error: any) {
|
||||
// Ignore "message is not modified" error (error code 400)
|
||||
if (error?.error_code === 400 && error?.description?.includes('message is not modified')) {
|
||||
// Message content is the same, ignore
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getSessionOrAnswer(
|
||||
@@ -181,57 +54,6 @@ export async function handleCallback(
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case ACTIONS.VIEW_SESSION: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix)
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch messages for this session
|
||||
await syncEngine.fetchMessages(session.id)
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
|
||||
const text = formatSessionDetailView(session, messages)
|
||||
const keyboard = createSessionDetailKeyboard(session)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback()
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.REFRESH_SESSION: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix)
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
await syncEngine.fetchMessages(session.id)
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
|
||||
const text = formatSessionDetailView(session, messages)
|
||||
const keyboard = createSessionDetailKeyboard(session)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback('Refreshed')
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.BACK_TO_LIST: {
|
||||
const sessions = syncEngine.getActiveSessions()
|
||||
const text = formatSessionList(sessions)
|
||||
const keyboard = createSessionListKeyboard(sessions)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback()
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.REFRESH_LIST: {
|
||||
const sessions = syncEngine.getActiveSessions()
|
||||
const text = formatSessionList(sessions)
|
||||
const keyboard = createSessionListKeyboard(sessions)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback('Refreshed')
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.APPROVE: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix, { requireActive: true })
|
||||
if (!session) {
|
||||
@@ -247,66 +69,8 @@ export async function handleCallback(
|
||||
await syncEngine.approvePermission(session.id, requestId)
|
||||
await ctx.answerCallback('Approved!')
|
||||
|
||||
// Refresh the view
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSessionDetailView(updatedSession, messages)
|
||||
const keyboard = createSessionDetailKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.APPROVE_EDITS: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix, { requireActive: true })
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = findRequestByPrefix(session, extra || '')
|
||||
if (!requestId) {
|
||||
await ctx.answerCallback('Request not found or already processed')
|
||||
return
|
||||
}
|
||||
|
||||
await syncEngine.approvePermission(session.id, requestId, 'acceptEdits')
|
||||
await ctx.answerCallback('Approved with Accept Edits!')
|
||||
|
||||
// Refresh the view
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSessionDetailView(updatedSession, messages)
|
||||
const keyboard = createSessionDetailKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.APPROVE_BYPASS: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix, { requireActive: true })
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = findRequestByPrefix(session, extra || '')
|
||||
if (!requestId) {
|
||||
await ctx.answerCallback('Request not found or already processed')
|
||||
return
|
||||
}
|
||||
|
||||
await syncEngine.approvePermission(session.id, requestId, 'bypassPermissions')
|
||||
await ctx.answerCallback('Approved with Bypass!')
|
||||
|
||||
// Refresh the view
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSessionDetailView(updatedSession, messages)
|
||||
const keyboard = createSessionDetailKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
// Update the notification message
|
||||
await ctx.editMessage('Permission approved.', new InlineKeyboard())
|
||||
break
|
||||
}
|
||||
|
||||
@@ -325,145 +89,8 @@ export async function handleCallback(
|
||||
await syncEngine.denyPermission(session.id, requestId)
|
||||
await ctx.answerCallback('Denied')
|
||||
|
||||
// Refresh the view
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSessionDetailView(updatedSession, messages)
|
||||
const keyboard = createSessionDetailKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.ABORT: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix, { requireActive: true })
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
await syncEngine.abortSession(session.id)
|
||||
await ctx.answerCallback('Session aborted')
|
||||
|
||||
// Refresh the view
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSessionDetailView(updatedSession, messages)
|
||||
const keyboard = createSessionDetailKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.SETTINGS: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix)
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = formatSettingsView(session)
|
||||
const keyboard = createSettingsKeyboard(session)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback()
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.BACK_TO_SESSION: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix)
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const messages = syncEngine.getSessionMessages(session.id)
|
||||
const text = formatSessionDetailView(session, messages)
|
||||
const keyboard = createSessionDetailKeyboard(session)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback()
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.SET_MODE_DEFAULT:
|
||||
case ACTIONS.SET_MODE_EDITS:
|
||||
case ACTIONS.SET_MODE_BYPASS:
|
||||
case ACTIONS.SET_MODE_PLAN: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix, { requireActive: true })
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const modeMap: Record<string, 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'> = {
|
||||
[ACTIONS.SET_MODE_DEFAULT]: 'default',
|
||||
[ACTIONS.SET_MODE_EDITS]: 'acceptEdits',
|
||||
[ACTIONS.SET_MODE_BYPASS]: 'bypassPermissions',
|
||||
[ACTIONS.SET_MODE_PLAN]: 'plan'
|
||||
}
|
||||
const mode = modeMap[action]
|
||||
|
||||
try {
|
||||
await syncEngine.setPermissionMode(session.id, mode)
|
||||
await ctx.answerCallback(`Mode set to ${mode}`)
|
||||
|
||||
// Refresh settings view
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSettingsView(updatedSession)
|
||||
const keyboard = createSettingsKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Callback] Failed to set mode:', error)
|
||||
await ctx.answerCallback('Failed to change mode')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.SET_MODEL_DEFAULT:
|
||||
case ACTIONS.SET_MODEL_SONNET:
|
||||
case ACTIONS.SET_MODEL_OPUS: {
|
||||
const session = await getSessionOrAnswer(ctx, syncEngine, sessionPrefix, { requireActive: true })
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
const modelMap: Record<string, 'default' | 'sonnet' | 'opus'> = {
|
||||
[ACTIONS.SET_MODEL_DEFAULT]: 'default',
|
||||
[ACTIONS.SET_MODEL_SONNET]: 'sonnet',
|
||||
[ACTIONS.SET_MODEL_OPUS]: 'opus'
|
||||
}
|
||||
const model = modelMap[action]
|
||||
|
||||
try {
|
||||
await syncEngine.setModelMode(session.id, model)
|
||||
await ctx.answerCallback(`Model set to ${model}`)
|
||||
|
||||
// Refresh settings view
|
||||
const updatedSession = syncEngine.getSession(session.id)
|
||||
if (updatedSession) {
|
||||
const text = formatSettingsView(updatedSession)
|
||||
const keyboard = createSettingsKeyboard(updatedSession)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Callback] Failed to set model:', error)
|
||||
await ctx.answerCallback('Failed to change model')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.REFRESH_MACHINES: {
|
||||
const machines = syncEngine.getOnlineMachines()
|
||||
const text = formatMachineList(machines)
|
||||
const keyboard = createMachineListKeyboard(machines)
|
||||
await safeEditMessage(ctx, text, keyboard)
|
||||
await ctx.answerCallback('Refreshed')
|
||||
break
|
||||
}
|
||||
|
||||
case ACTIONS.SPAWN_SESSION: {
|
||||
// Handled directly in bot.ts setupCallbacks()
|
||||
// This case shouldn't be reached, but handle gracefully
|
||||
await ctx.answerCallback('Use /new to create a session')
|
||||
// Update the notification message
|
||||
await ctx.editMessage('Permission denied.', new InlineKeyboard())
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Message Renderer for Telegram
|
||||
* Utilities for Telegram Bot
|
||||
*
|
||||
* Formats session data, messages, and other content for Telegram display.
|
||||
* Handles truncation to respect Telegram's message limits.
|
||||
* Helper functions for rendering and callback data handling.
|
||||
* Simplified to only include utilities needed for notifications.
|
||||
*/
|
||||
|
||||
import type { Session, Machine, DecryptedMessage } from '../sync/syncEngine'
|
||||
import type { Session } from '../sync/syncEngine'
|
||||
|
||||
// Telegram limits
|
||||
const MAX_MESSAGE_LENGTH = 4096
|
||||
@@ -19,37 +19,6 @@ export function truncate(text: string, maxLen: number = MAX_MESSAGE_LENGTH - 100
|
||||
return text.slice(0, maxLen - 3) + '...'
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters for Telegram MarkdownV2
|
||||
*/
|
||||
export function escapeMarkdown(text: string): string {
|
||||
return text.replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status emoji for a session
|
||||
*/
|
||||
export function getSessionStatusEmoji(session: Session): string {
|
||||
const hasRequests = session.agentState?.requests && Object.keys(session.agentState.requests).length > 0
|
||||
|
||||
if (hasRequests) return '🔔' // Permission needed
|
||||
if (session.thinking) return '💭' // Thinking
|
||||
if (session.active) return '🟢' // Active
|
||||
return '⚪' // Inactive
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status text for a session
|
||||
*/
|
||||
export function getSessionStatusText(session: Session): string {
|
||||
const hasRequests = session.agentState?.requests && Object.keys(session.agentState.requests).length > 0
|
||||
|
||||
if (hasRequests) return 'Permission needed'
|
||||
if (session.thinking) return 'Thinking'
|
||||
if (session.active) return 'Active'
|
||||
return 'Inactive'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session name (project name or directory name)
|
||||
*/
|
||||
@@ -62,218 +31,6 @@ export function getSessionName(session: Session): string {
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Format session list for display
|
||||
*/
|
||||
export function formatSessionList(sessions: Session[]): string {
|
||||
if (sessions.length === 0) {
|
||||
return 'No active sessions.\n\nUse /app to open the Mini App and create a new session.'
|
||||
}
|
||||
|
||||
let message = `Active Sessions (${sessions.length}):\n\n`
|
||||
|
||||
// Sort sessions: permission needed first, then by activity
|
||||
const sorted = [...sessions].sort((a, b) => {
|
||||
const aHasReq = a.agentState?.requests && Object.keys(a.agentState.requests).length > 0
|
||||
const bHasReq = b.agentState?.requests && Object.keys(b.agentState.requests).length > 0
|
||||
|
||||
if (aHasReq && !bHasReq) return -1
|
||||
if (!aHasReq && bHasReq) return 1
|
||||
if (a.thinking && !b.thinking) return -1
|
||||
if (!a.thinking && b.thinking) return 1
|
||||
if (a.active && !b.active) return -1
|
||||
if (!a.active && b.active) return 1
|
||||
return b.activeAt - a.activeAt
|
||||
})
|
||||
|
||||
// Show up to 10 sessions
|
||||
const display = sorted.slice(0, 10)
|
||||
|
||||
for (let i = 0; i < display.length; i++) {
|
||||
const session = display[i]
|
||||
const num = i + 1
|
||||
const emoji = getSessionStatusEmoji(session)
|
||||
const name = getSessionName(session)
|
||||
const status = getSessionStatusText(session)
|
||||
const path = session.metadata?.path || ''
|
||||
|
||||
message += `${num}. ${emoji} ${name}\n`
|
||||
if (path) {
|
||||
message += ` ${truncate(path, 50)}\n`
|
||||
}
|
||||
message += ` ${status}\n\n`
|
||||
}
|
||||
|
||||
if (sessions.length > 10) {
|
||||
message += `... and ${sessions.length - 10} more sessions`
|
||||
}
|
||||
|
||||
return truncate(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format session detail for display
|
||||
*/
|
||||
export function formatSessionDetail(session: Session, messages: DecryptedMessage[] = []): string {
|
||||
const name = getSessionName(session)
|
||||
const status = getSessionStatusText(session)
|
||||
const statusEmoji = getSessionStatusEmoji(session)
|
||||
const path = session.metadata?.path || 'Unknown'
|
||||
const host = session.metadata?.host || 'Unknown'
|
||||
const mode = session.permissionMode || 'default'
|
||||
|
||||
let message = `${statusEmoji} ${name}\n\n`
|
||||
message += `Path: ${path}\n`
|
||||
message += `Host: ${host}\n`
|
||||
message += `Status: ${status}\n`
|
||||
message += `Mode: ${mode}\n`
|
||||
|
||||
// Check for permission requests
|
||||
const requests = session.agentState?.requests
|
||||
if (requests && Object.keys(requests).length > 0) {
|
||||
message += '\n--- Permission Request ---\n'
|
||||
for (const [reqId, req] of Object.entries(requests)) {
|
||||
message += `Tool: ${req.tool}\n`
|
||||
if (req.arguments) {
|
||||
const args = formatToolArguments(req.tool, req.arguments)
|
||||
if (args) {
|
||||
message += `${args}\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show recent messages if available
|
||||
if (messages.length > 0) {
|
||||
message += '\n--- Recent Messages ---\n'
|
||||
const recent = messages.slice(-5) // Last 5 messages
|
||||
|
||||
for (const msg of recent) {
|
||||
const formatted = formatMessage(msg)
|
||||
if (formatted) {
|
||||
message += formatted + '\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add summary if available
|
||||
if (session.metadata?.summary?.text) {
|
||||
message += '\n--- Summary ---\n'
|
||||
message += truncate(session.metadata.summary.text, 500) + '\n'
|
||||
}
|
||||
|
||||
return truncate(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tool arguments for display
|
||||
*/
|
||||
function formatToolArguments(tool: string, args: any): string {
|
||||
try {
|
||||
switch (tool) {
|
||||
case 'Edit':
|
||||
case 'Write':
|
||||
case 'Read':
|
||||
return `File: ${args.file_path || args.path || 'unknown'}`
|
||||
case 'Bash':
|
||||
const cmd = args.command || ''
|
||||
return `Command: ${truncate(cmd, 100)}`
|
||||
case 'Task':
|
||||
return `Task: ${truncate(args.prompt || args.description || '', 100)}`
|
||||
case 'Grep':
|
||||
case 'Glob':
|
||||
return `Pattern: ${args.pattern || ''}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single message for display
|
||||
*/
|
||||
function formatMessage(msg: DecryptedMessage): string {
|
||||
try {
|
||||
const content = msg.content
|
||||
if (!content || typeof content !== 'object') return ''
|
||||
|
||||
const contentObj = content as Record<string, unknown>
|
||||
|
||||
const role = typeof contentObj.role === 'string' ? contentObj.role : 'unknown'
|
||||
const roleEmoji = role === 'user' ? '👤' : role === 'assistant' ? '🤖' : '🔧'
|
||||
|
||||
const inner = contentObj.content
|
||||
|
||||
// Handle different content types
|
||||
if (inner && typeof inner === 'object') {
|
||||
const innerObj = inner as Record<string, unknown>
|
||||
if (innerObj.type === 'text') {
|
||||
const text = typeof innerObj.text === 'string' ? innerObj.text : ''
|
||||
return `${roleEmoji} ${truncate(text, 200)}`
|
||||
}
|
||||
|
||||
if (innerObj.type === 'tool_use') {
|
||||
const toolName = typeof innerObj.name === 'string' ? innerObj.name : 'unknown'
|
||||
return `🔧 ${toolName}`
|
||||
}
|
||||
|
||||
if (innerObj.type === 'tool_result') {
|
||||
return `✓ Tool completed`
|
||||
}
|
||||
}
|
||||
|
||||
if (inner && typeof inner === 'object') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof inner === 'string') {
|
||||
return `${roleEmoji} ${truncate(inner, 200)}`
|
||||
}
|
||||
|
||||
// Backward-compat fallback for older content shapes
|
||||
if (typeof (contentObj as { content?: unknown }).content === 'string') {
|
||||
const text = (contentObj as { content: string }).content
|
||||
return `${roleEmoji} ${truncate(text, 200)}`
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format machine list for display
|
||||
*/
|
||||
export function formatMachineList(machines: Machine[]): string {
|
||||
if (machines.length === 0) {
|
||||
return 'No machines online.\n\nMake sure you have the HAPI daemon running on your machines.'
|
||||
}
|
||||
|
||||
let message = `Online Machines (${machines.length}):\n\n`
|
||||
|
||||
for (let i = 0; i < machines.length; i++) {
|
||||
const machine = machines[i]
|
||||
const num = i + 1
|
||||
const name = machine.metadata?.displayName || machine.metadata?.host || 'Unknown'
|
||||
const platform = machine.metadata?.platform || ''
|
||||
const version = machine.metadata?.happyCliVersion || ''
|
||||
|
||||
message += `${num}. 🟢 ${name}\n`
|
||||
if (platform) {
|
||||
message += ` Platform: ${platform}\n`
|
||||
}
|
||||
if (version) {
|
||||
message += ` Version: ${version}\n`
|
||||
}
|
||||
message += '\n'
|
||||
}
|
||||
|
||||
return truncate(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create callback data with size limit
|
||||
* Format: action:sessionIdPrefix:extraData
|
||||
@@ -312,10 +69,3 @@ export function parseCallbackData(data: string): { action: string; sessionPrefix
|
||||
export function findSessionByPrefix(sessions: Session[], prefix: string): Session | undefined {
|
||||
return sessions.find(s => s.id.startsWith(prefix))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find machine by ID prefix
|
||||
*/
|
||||
export function findMachineByPrefix(machines: Machine[], prefix: string): Machine | undefined {
|
||||
return machines.find(m => m.id.startsWith(prefix))
|
||||
}
|
||||
|
||||
@@ -1,140 +1,74 @@
|
||||
/**
|
||||
* Session Detail View for Telegram
|
||||
* Session Notification View for Telegram
|
||||
*
|
||||
* Provides detailed session information display including:
|
||||
* - Session metadata (path, host, status, mode)
|
||||
* - Recent messages with proper formatting
|
||||
* - Permission request details
|
||||
* - Session summary
|
||||
* Provides notification formatting for permission requests.
|
||||
* All interactive session views are handled by the Telegram Mini App.
|
||||
*/
|
||||
|
||||
import { InlineKeyboard } from 'grammy'
|
||||
import type { Session, DecryptedMessage, SyncEngine } from '../sync/syncEngine'
|
||||
import { configuration } from '../configuration'
|
||||
import type { Session } from '../sync/syncEngine'
|
||||
import { ACTIONS } from './callbacks'
|
||||
import { createCallbackData, truncate, getSessionStatusEmoji, getSessionStatusText, getSessionName } from './renderer'
|
||||
import { createCallbackData, truncate, getSessionName } from './renderer'
|
||||
|
||||
// Maximum message display
|
||||
const MAX_MESSAGES_DISPLAY = 8
|
||||
const MAX_MESSAGE_LENGTH = 300
|
||||
const MAX_TOOL_ARGS_LENGTH = 150
|
||||
|
||||
/**
|
||||
* Format session detail view with full information
|
||||
* Format a compact session notification for permission requests
|
||||
*/
|
||||
export function formatSessionDetailView(
|
||||
session: Session,
|
||||
messages: DecryptedMessage[] = []
|
||||
): string {
|
||||
const lines: string[] = []
|
||||
|
||||
// Header
|
||||
export function formatSessionNotification(session: Session): string {
|
||||
const name = getSessionName(session)
|
||||
const emoji = getSessionStatusEmoji(session)
|
||||
lines.push(`${emoji} ${name}`)
|
||||
lines.push('')
|
||||
const lines: string[] = ['Permission Request', '', `Session: ${name}`]
|
||||
|
||||
// Metadata
|
||||
if (session.metadata?.path) {
|
||||
lines.push(`Path: ${session.metadata.path}`)
|
||||
}
|
||||
if (session.metadata?.host) {
|
||||
lines.push(`Host: ${session.metadata.host}`)
|
||||
}
|
||||
|
||||
// Status line
|
||||
const status = getSessionStatusText(session)
|
||||
const mode = formatPermissionMode(session.permissionMode)
|
||||
const model = formatModelMode(session.modelMode)
|
||||
lines.push(`Status: ${status} | Mode: ${mode}${model ? ` | ${model}` : ''}`)
|
||||
lines.push('')
|
||||
|
||||
// Permission requests
|
||||
const requests = session.agentState?.requests
|
||||
if (requests && Object.keys(requests).length > 0) {
|
||||
lines.push('--- Permission Request ---')
|
||||
for (const [reqId, req] of Object.entries(requests)) {
|
||||
if (requests) {
|
||||
const reqId = Object.keys(requests)[0]
|
||||
const req = requests[reqId]
|
||||
if (req) {
|
||||
lines.push(`Tool: ${req.tool}`)
|
||||
const argsDisplay = formatToolArgumentsDetailed(req.tool, req.arguments)
|
||||
if (argsDisplay) {
|
||||
lines.push(argsDisplay)
|
||||
const args = formatToolArgumentsDetailed(req.tool, req.arguments)
|
||||
if (args) {
|
||||
lines.push(args)
|
||||
}
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// Recent messages
|
||||
if (messages.length > 0) {
|
||||
lines.push('--- Recent Messages ---')
|
||||
const recentMessages = messages.slice(-MAX_MESSAGES_DISPLAY)
|
||||
|
||||
for (const msg of recentMessages) {
|
||||
const formatted = formatMessageDetailed(msg)
|
||||
if (formatted) {
|
||||
lines.push(formatted)
|
||||
}
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// Summary
|
||||
if (session.metadata?.summary?.text) {
|
||||
lines.push('--- Summary ---')
|
||||
const summaryText = truncate(session.metadata.summary.text, 500)
|
||||
lines.push(summaryText)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// Activity info
|
||||
if (session.activeAt) {
|
||||
const lastActive = formatTimeAgo(session.activeAt)
|
||||
lines.push(`Last active: ${lastActive}`)
|
||||
}
|
||||
|
||||
return truncate(lines.join('\n'), 4000)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create session detail keyboard with contextual actions
|
||||
* Create notification keyboard for quick actions
|
||||
*/
|
||||
export function createSessionDetailKeyboard(session: Session): InlineKeyboard {
|
||||
export function createNotificationKeyboard(session: Session, miniAppUrl: string): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
const hasRequests = session.agentState?.requests && Object.keys(session.agentState.requests).length > 0
|
||||
const requests = session.agentState?.requests ?? null
|
||||
const hasRequests = Boolean(requests && Object.keys(requests).length > 0)
|
||||
const canControl = session.active
|
||||
|
||||
// Permission buttons if there are pending requests
|
||||
if (canControl && hasRequests) {
|
||||
const requestId = Object.keys(session.agentState!.requests!)[0]
|
||||
const requestId = Object.keys(requests!)[0]
|
||||
const reqPrefix = requestId.slice(0, 8)
|
||||
|
||||
keyboard
|
||||
.text('✅ Allow', createCallbackData(ACTIONS.APPROVE, session.id, reqPrefix))
|
||||
.text('✅✅ Edits', createCallbackData(ACTIONS.APPROVE_EDITS, session.id, reqPrefix))
|
||||
.text('❌ Deny', createCallbackData(ACTIONS.DENY, session.id, reqPrefix))
|
||||
.text('Allow', createCallbackData(ACTIONS.APPROVE, session.id, reqPrefix))
|
||||
.text('Deny', createCallbackData(ACTIONS.DENY, session.id, reqPrefix))
|
||||
keyboard.row()
|
||||
|
||||
// Add bypass option
|
||||
keyboard.text('⚡ Bypass All', createCallbackData(ACTIONS.APPROVE_BYPASS, session.id, reqPrefix))
|
||||
keyboard.row()
|
||||
keyboard.webApp(
|
||||
'Details',
|
||||
buildMiniAppDeepLink(miniAppUrl, `session_${session.id}`)
|
||||
)
|
||||
return keyboard
|
||||
}
|
||||
|
||||
// Control buttons
|
||||
keyboard.text('🔄 Refresh', createCallbackData(ACTIONS.REFRESH_SESSION, session.id))
|
||||
if (canControl) {
|
||||
keyboard.text('⏹ Abort', createCallbackData(ACTIONS.ABORT, session.id))
|
||||
}
|
||||
keyboard.row()
|
||||
|
||||
// Settings and navigation
|
||||
keyboard
|
||||
.text('⚙️ Settings', createCallbackData(ACTIONS.SETTINGS, session.id))
|
||||
.text('← Back', createCallbackData(ACTIONS.BACK_TO_LIST, 'back'))
|
||||
|
||||
keyboard.webApp(
|
||||
'Open Session',
|
||||
buildMiniAppDeepLink(miniAppUrl, `session_${session.id}`)
|
||||
)
|
||||
return keyboard
|
||||
}
|
||||
|
||||
/**
|
||||
* Format detailed tool arguments
|
||||
* Format detailed tool arguments for notification display
|
||||
*/
|
||||
function formatToolArgumentsDetailed(tool: string, args: any): string {
|
||||
if (!args) return ''
|
||||
@@ -205,203 +139,6 @@ function formatToolArgumentsDetailed(tool: string, args: any): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a message with more detail
|
||||
*/
|
||||
function formatMessageDetailed(msg: DecryptedMessage): string {
|
||||
if (!msg.content || typeof msg.content !== 'object') return ''
|
||||
|
||||
try {
|
||||
const contentObj = msg.content as Record<string, unknown>
|
||||
const role = typeof contentObj.role === 'string' ? contentObj.role : 'unknown'
|
||||
const roleEmoji = role === 'user' ? '👤' : role === 'assistant' ? '🤖' : '🔧'
|
||||
|
||||
const inner = contentObj.content
|
||||
|
||||
if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
|
||||
const innerObj = inner as Record<string, unknown>
|
||||
|
||||
if (innerObj.type === 'text') {
|
||||
const text = typeof innerObj.text === 'string' ? innerObj.text : ''
|
||||
return `${roleEmoji} ${truncate(text, MAX_MESSAGE_LENGTH)}`
|
||||
}
|
||||
|
||||
if (innerObj.type === 'tool_use') {
|
||||
const toolName = typeof innerObj.name === 'string' ? innerObj.name : 'unknown'
|
||||
let display = `🔧 ${toolName}`
|
||||
|
||||
const input = innerObj.input
|
||||
if (input && typeof input === 'object') {
|
||||
const inputObj = input as Record<string, unknown>
|
||||
if (typeof inputObj.file_path === 'string') {
|
||||
const fileName = inputObj.file_path.split('/').pop()
|
||||
display += `: ${fileName}`
|
||||
} else if (typeof inputObj.command === 'string') {
|
||||
display += `: ${truncate(inputObj.command, 50)}`
|
||||
} else if (typeof inputObj.pattern === 'string') {
|
||||
display += `: ${inputObj.pattern}`
|
||||
}
|
||||
}
|
||||
|
||||
return display
|
||||
}
|
||||
|
||||
if (innerObj.type === 'tool_result') {
|
||||
const isError = Boolean(innerObj.is_error)
|
||||
return isError ? '❌ Tool failed' : '✓ Tool completed'
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(inner)) {
|
||||
const first = inner[0]
|
||||
if (first && typeof first === 'object') {
|
||||
const firstObj = first as Record<string, unknown>
|
||||
if (firstObj.type === 'text') {
|
||||
const text = typeof firstObj.text === 'string' ? firstObj.text : ''
|
||||
return `${roleEmoji} ${truncate(text, MAX_MESSAGE_LENGTH)}`
|
||||
}
|
||||
if (firstObj.type === 'tool_use') {
|
||||
const name = typeof firstObj.name === 'string' ? firstObj.name : 'Tool'
|
||||
return `🔧 ${name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof inner === 'string') {
|
||||
return `${roleEmoji} ${truncate(inner, MAX_MESSAGE_LENGTH)}`
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format permission mode for display
|
||||
*/
|
||||
function formatPermissionMode(mode?: string | null): string {
|
||||
switch (mode) {
|
||||
case 'acceptEdits':
|
||||
return 'Accept Edits'
|
||||
case 'bypassPermissions':
|
||||
return 'Bypass'
|
||||
case 'plan':
|
||||
return 'Plan'
|
||||
default:
|
||||
return 'Default'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format model mode for display
|
||||
*/
|
||||
function formatModelMode(mode?: string | null): string {
|
||||
switch (mode) {
|
||||
case 'sonnet':
|
||||
return 'Sonnet'
|
||||
case 'opus':
|
||||
return 'Opus'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp as time ago
|
||||
*/
|
||||
function formatTimeAgo(timestamp: number): string {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) return `${days}d ago`
|
||||
if (hours > 0) return `${hours}h ago`
|
||||
if (minutes > 0) return `${minutes}m ago`
|
||||
if (seconds > 10) return `${seconds}s ago`
|
||||
return 'just now'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a compact session card for notifications
|
||||
*/
|
||||
export function formatSessionNotification(
|
||||
session: Session,
|
||||
eventType: 'permission' | 'message' | 'status'
|
||||
): string {
|
||||
const name = getSessionName(session)
|
||||
const emoji = getSessionStatusEmoji(session)
|
||||
|
||||
let title = ''
|
||||
let details = ''
|
||||
|
||||
switch (eventType) {
|
||||
case 'permission': {
|
||||
title = 'Permission Request'
|
||||
const requests = session.agentState?.requests
|
||||
if (requests) {
|
||||
const reqId = Object.keys(requests)[0]
|
||||
const req = requests[reqId]
|
||||
if (req) {
|
||||
details = `Tool: ${req.tool}`
|
||||
const args = formatToolArgumentsDetailed(req.tool, req.arguments)
|
||||
if (args) {
|
||||
details += `\n${args}`
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'message':
|
||||
title = '💬 New Message'
|
||||
break
|
||||
|
||||
case 'status':
|
||||
title = `${emoji} Status Update`
|
||||
details = getSessionStatusText(session)
|
||||
break
|
||||
}
|
||||
|
||||
return `${title}\n\nSession: ${name}\n${details}`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification keyboard for quick actions
|
||||
*/
|
||||
export function createNotificationKeyboard(session: Session): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
const requests = session.agentState?.requests ?? null
|
||||
const hasRequests = Boolean(requests && Object.keys(requests).length > 0)
|
||||
const canControl = session.active
|
||||
|
||||
if (canControl && hasRequests) {
|
||||
const requestId = Object.keys(requests!)[0]
|
||||
const reqPrefix = requestId.slice(0, 8)
|
||||
|
||||
keyboard
|
||||
.text('✅ Allow', createCallbackData(ACTIONS.APPROVE, session.id, reqPrefix))
|
||||
.text('❌ Deny', createCallbackData(ACTIONS.DENY, session.id, reqPrefix))
|
||||
keyboard.row()
|
||||
|
||||
keyboard.webApp(
|
||||
'Details',
|
||||
buildMiniAppDeepLink(configuration.miniAppUrl, `session_${session.id}`)
|
||||
)
|
||||
return keyboard
|
||||
}
|
||||
|
||||
keyboard.webApp(
|
||||
'Open Session',
|
||||
buildMiniAppDeepLink(configuration.miniAppUrl, `session_${session.id}`)
|
||||
)
|
||||
return keyboard
|
||||
}
|
||||
|
||||
function buildMiniAppDeepLink(baseUrl: string, startParam: string): string {
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
@@ -412,79 +149,3 @@ function buildMiniAppDeepLink(baseUrl: string, startParam: string): string {
|
||||
return `${baseUrl}${separator}startapp=${encodeURIComponent(startParam)}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format settings view for a session
|
||||
*/
|
||||
export function formatSettingsView(session: Session): string {
|
||||
const name = getSessionName(session)
|
||||
const currentMode = formatPermissionMode(session.permissionMode)
|
||||
const currentModel = session.modelMode || 'default'
|
||||
|
||||
const lines = [
|
||||
`⚙️ Settings: ${name}`,
|
||||
'',
|
||||
`Permission Mode: ${currentMode}`,
|
||||
`Model: ${currentModel === 'default' ? 'Default' : currentModel === 'sonnet' ? 'Sonnet' : 'Opus'}`,
|
||||
'',
|
||||
'Select options below to change settings.'
|
||||
]
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create settings keyboard
|
||||
*/
|
||||
export function createSettingsKeyboard(session: Session): InlineKeyboard {
|
||||
const keyboard = new InlineKeyboard()
|
||||
if (!session.active) {
|
||||
keyboard.text('← Back to Session', createCallbackData(ACTIONS.BACK_TO_SESSION, session.id))
|
||||
return keyboard
|
||||
}
|
||||
|
||||
const currentMode = session.permissionMode || 'default'
|
||||
const currentModel = session.modelMode || 'default'
|
||||
|
||||
// Permission mode row 1
|
||||
keyboard.text(
|
||||
currentMode === 'default' ? '✓ Default' : 'Default',
|
||||
createCallbackData(ACTIONS.SET_MODE_DEFAULT, session.id)
|
||||
)
|
||||
keyboard.text(
|
||||
currentMode === 'acceptEdits' ? '✓ Accept Edits' : 'Accept Edits',
|
||||
createCallbackData(ACTIONS.SET_MODE_EDITS, session.id)
|
||||
)
|
||||
keyboard.row()
|
||||
|
||||
// Permission mode row 2
|
||||
keyboard.text(
|
||||
currentMode === 'bypassPermissions' ? '✓ Bypass' : 'Bypass',
|
||||
createCallbackData(ACTIONS.SET_MODE_BYPASS, session.id)
|
||||
)
|
||||
keyboard.text(
|
||||
currentMode === 'plan' ? '✓ Plan' : 'Plan',
|
||||
createCallbackData(ACTIONS.SET_MODE_PLAN, session.id)
|
||||
)
|
||||
keyboard.row()
|
||||
|
||||
// Model row
|
||||
keyboard.text(
|
||||
currentModel === 'sonnet' ? '✓ Sonnet' : 'Sonnet',
|
||||
createCallbackData(ACTIONS.SET_MODEL_SONNET, session.id)
|
||||
)
|
||||
keyboard.text(
|
||||
currentModel === 'opus' ? '✓ Opus' : 'Opus',
|
||||
createCallbackData(ACTIONS.SET_MODEL_OPUS, session.id)
|
||||
)
|
||||
keyboard.row()
|
||||
|
||||
// Actions
|
||||
keyboard.text('⏹ Abort Session', createCallbackData(ACTIONS.ABORT, session.id))
|
||||
keyboard.row()
|
||||
|
||||
// Back button
|
||||
keyboard.text('← Back to Session', createCallbackData(ACTIONS.BACK_TO_SESSION, session.id))
|
||||
|
||||
return keyboard
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user