Skip to content

Commit 9402685

Browse files
heavygeeHAPI
andcommitted
fix(web): use session flavor label in voice context formatters
Replaces hardcoded 'Claude Code' strings in voice context injections with the active session's flavor label (Cursor, Codex, Gemini, etc.) via getFlavorLabel() from @hapi/protocol. Falls back to 'coding agent' for unknown or missing flavors. Threads an agentLabel param through formatMessage, formatPermissionRequest, formatReadyEvent, formatNewMessages, formatHistory, and formatSessionFull. voiceHooks resolves the label once per call via session.metadata.flavor. Fixes tiann#680 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
1 parent 3258c52 commit 9402685

3 files changed

Lines changed: 111 additions & 29 deletions

File tree

web/src/realtime/hooks/contextFormatters.test.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { describe, expect, it } from 'vitest'
22
import type { DecryptedMessage } from '@/types/api'
3-
import { extractLastAssistantSpeakable, formatMessage, formatNewMessages, formatReadyEvent } from './contextFormatters'
3+
import {
4+
extractLastAssistantSpeakable,
5+
formatMessage,
6+
formatNewMessages,
7+
formatPermissionRequest,
8+
formatReadyEvent,
9+
} from './contextFormatters'
410

511
function msg(partial: Pick<DecryptedMessage, 'id' | 'seq' | 'content'>): DecryptedMessage {
612
return {
@@ -122,6 +128,17 @@ describe('formatReadyEvent', () => {
122128
const event = formatReadyEvent(sessionId, ' ')
123129
expect(event).toContain('Use the latest agent message already present in context')
124130
})
131+
132+
it('uses the provided agent label', () => {
133+
const event = formatReadyEvent(sessionId, null, 'Codex')
134+
expect(event).toContain('Codex finished working')
135+
expect(event).not.toContain('Claude Code')
136+
})
137+
138+
it('defaults to coding agent label', () => {
139+
const event = formatReadyEvent(sessionId)
140+
expect(event).toContain('coding agent finished working')
141+
})
125142
})
126143

127144
describe('formatMessage', () => {
@@ -141,7 +158,7 @@ describe('formatMessage', () => {
141158
}
142159
}))
143160

144-
expect(formatted).toContain('Claude Code:')
161+
expect(formatted).toContain('coding agent:')
145162
expect(formatted).toContain('<text>Indexed 5,018 items in the search database.</text>')
146163
})
147164

@@ -188,7 +205,54 @@ describe('formatMessage', () => {
188205
}))
189206

190207
expect(formatted).toContain('Here is the result.')
191-
expect(formatted).toContain('Claude Code is using Bash')
208+
expect(formatted).toContain('coding agent is using Bash')
209+
})
210+
211+
it('uses the provided label for assistant text', () => {
212+
const formatted = formatMessage(
213+
msg({ id: '1', seq: 1, content: { role: 'assistant', content: 'Refactor complete.' } }),
214+
'Cursor'
215+
)
216+
expect(formatted).toContain('Cursor:')
217+
expect(formatted).toContain('Refactor complete.')
218+
expect(formatted).not.toContain('Claude Code')
219+
})
220+
221+
it('defaults to coding agent when no label is given', () => {
222+
const formatted = formatMessage(
223+
msg({ id: '1', seq: 1, content: { role: 'assistant', content: 'Done.' } })
224+
)
225+
expect(formatted).toContain('coding agent:')
226+
expect(formatted).not.toContain('Claude Code')
227+
})
228+
229+
it('uses the provided label for tool-call lines', () => {
230+
const formatted = formatMessage(
231+
msg({
232+
id: '1',
233+
seq: 1,
234+
content: {
235+
role: 'assistant',
236+
content: [{ type: 'tool_use', name: 'Bash', input: { command: 'ls' } }]
237+
}
238+
}),
239+
'Gemini'
240+
)
241+
expect(formatted).toContain('Gemini is using Bash')
242+
expect(formatted).not.toContain('Claude Code')
243+
})
244+
})
245+
246+
describe('formatPermissionRequest', () => {
247+
it('uses the provided label', () => {
248+
const result = formatPermissionRequest('sid', 'rid', 'Bash', {}, 'OpenCode')
249+
expect(result).toContain('OpenCode is requesting permission')
250+
expect(result).not.toContain('Claude Code')
251+
})
252+
253+
it('defaults to coding agent', () => {
254+
const result = formatPermissionRequest('sid', 'rid', 'Bash', {})
255+
expect(result).toContain('coding agent is requesting permission')
192256
})
193257
})
194258

@@ -214,4 +278,12 @@ describe('formatNewMessages', () => {
214278
expect(update).toContain('New messages in session: session-1')
215279
expect(update).toContain('Local database file size is 2.43 GiB.')
216280
})
281+
282+
it('uses the provided label in formatted message output', () => {
283+
const result = formatNewMessages('session-1', [
284+
msg({ id: '1', seq: 1, content: { role: 'assistant', content: 'Build succeeded.' } })
285+
], 'Cursor')
286+
expect(result).toContain('Cursor:')
287+
expect(result).not.toContain('Claude Code')
288+
})
217289
})

web/src/realtime/hooks/contextFormatters.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ function unwrapOutputContent(content: unknown): { roleOverride: NormalizedRole |
6666
return { roleOverride, content: messageContent }
6767
}
6868

69-
function formatPlainText(role: NormalizedRole | null, text: string): string {
69+
function formatPlainText(role: NormalizedRole | null, text: string, agentLabel = 'coding agent'): string {
7070
if (role === 'assistant') {
71-
return `Claude Code: \n<text>${text}</text>`
71+
return `${agentLabel}: \n<text>${text}</text>`
7272
}
7373
return `User sent message: \n<text>${text}</text>`
7474
}
@@ -80,9 +80,10 @@ export function formatPermissionRequest(
8080
sessionId: string,
8181
requestId: string,
8282
toolName: string,
83-
toolArgs: unknown
83+
toolArgs: unknown,
84+
agentLabel = 'coding agent'
8485
): string {
85-
return `Claude Code is requesting permission to use ${toolName} (session ${sessionId}):
86+
return `${agentLabel} is requesting permission to use ${toolName} (session ${sessionId}):
8687
<request_id>${requestId}</request_id>
8788
<tool_name>${toolName}</tool_name>
8889
<tool_args>${JSON.stringify(toolArgs)}</tool_args>`
@@ -91,7 +92,7 @@ export function formatPermissionRequest(
9192
/**
9293
* Format a single message for voice context
9394
*/
94-
export function formatMessage(message: DecryptedMessage): string | null {
95+
export function formatMessage(message: DecryptedMessage, agentLabel = 'coding agent'): string | null {
9596
const { role, content: wrappedContent } = unwrapRoleWrappedContent(message)
9697
const { roleOverride, content } = unwrapOutputContent(wrappedContent)
9798
const normalizedRole = roleOverride ?? role
@@ -103,7 +104,7 @@ export function formatMessage(message: DecryptedMessage): string | null {
103104
const speakable = !isContentArray(content) ? extractSpeakableFromContent(content) : null
104105
if (speakable) {
105106
const roleForFormat = normalizedRole === 'user' ? 'user' : 'assistant'
106-
return formatPlainText(roleForFormat, speakable)
107+
return formatPlainText(roleForFormat, speakable, agentLabel)
107108
}
108109

109110
if (!isContentArray(content)) {
@@ -122,13 +123,13 @@ export function formatMessage(message: DecryptedMessage): string | null {
122123

123124
for (const item of content) {
124125
if (item.type === 'text' && item.text) {
125-
lines.push(formatPlainText(isAssistant ? 'assistant' : 'user', item.text))
126+
lines.push(formatPlainText(isAssistant ? 'assistant' : 'user', item.text, agentLabel))
126127
} else if (item.type === 'tool_use' && !VOICE_CONFIG.DISABLE_TOOL_CALLS) {
127128
const name = item.name || 'unknown'
128129
if (VOICE_CONFIG.LIMITED_TOOL_CALLS) {
129-
lines.push(`Claude Code is using ${name}`)
130+
lines.push(`${agentLabel} is using ${name}`)
130131
} else {
131-
lines.push(`Claude Code is using ${name} with arguments: <arguments>${JSON.stringify(item.input)}</arguments>`)
132+
lines.push(`${agentLabel} is using ${name} with arguments: <arguments>${JSON.stringify(item.input)}</arguments>`)
132133
}
133134
}
134135
}
@@ -214,34 +215,34 @@ export function extractLastAssistantSpeakable(messages: DecryptedMessage[]): str
214215
return null
215216
}
216217

217-
export function formatNewSingleMessage(sessionId: string, message: DecryptedMessage): string | null {
218-
const formatted = formatMessage(message)
218+
export function formatNewSingleMessage(sessionId: string, message: DecryptedMessage, agentLabel = 'coding agent'): string | null {
219+
const formatted = formatMessage(message, agentLabel)
219220
if (!formatted) {
220221
return null
221222
}
222223
return 'New message in session: ' + sessionId + '\n\n' + formatted
223224
}
224225

225-
export function formatNewMessages(sessionId: string, messages: DecryptedMessage[]): string | null {
226+
export function formatNewMessages(sessionId: string, messages: DecryptedMessage[], agentLabel = 'coding agent'): string | null {
226227
const formatted = [...messages]
227228
.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0))
228-
.map(formatMessage)
229+
.map(m => formatMessage(m, agentLabel))
229230
.filter(Boolean)
230231
if (formatted.length === 0) {
231232
return null
232233
}
233234
return 'New messages in session: ' + sessionId + '\n\n' + formatted.join('\n\n')
234235
}
235236

236-
export function formatHistory(sessionId: string, messages: DecryptedMessage[]): string {
237+
export function formatHistory(sessionId: string, messages: DecryptedMessage[], agentLabel = 'coding agent'): string {
237238
const messagesToFormat = VOICE_CONFIG.MAX_HISTORY_MESSAGES > 0
238239
? messages.slice(-VOICE_CONFIG.MAX_HISTORY_MESSAGES)
239240
: messages
240-
const formatted = messagesToFormat.map(formatMessage).filter(Boolean)
241+
const formatted = messagesToFormat.map(m => formatMessage(m, agentLabel)).filter(Boolean)
241242
return 'History of messages in session: ' + sessionId + '\n\n' + formatted.join('\n\n')
242243
}
243244

244-
export function formatSessionFull(session: Session | null, messages: DecryptedMessage[]): string {
245+
export function formatSessionFull(session: Session | null, messages: DecryptedMessage[], agentLabel = 'coding agent'): string {
245246
if (!session) {
246247
return 'Session not available'
247248
}
@@ -262,7 +263,7 @@ export function formatSessionFull(session: Session | null, messages: DecryptedMe
262263

263264
lines.push('## Our interaction history so far')
264265
lines.push('')
265-
lines.push(formatHistory(session.id, messages))
266+
lines.push(formatHistory(session.id, messages, agentLabel))
266267

267268
return lines.join('\n\n')
268269
}
@@ -279,10 +280,10 @@ export function formatSessionFocus(sessionId: string, _metadata?: SessionMetadat
279280
return `Session became focused: ${sessionId}`
280281
}
281282

282-
export function formatReadyEvent(sessionId: string, lastAssistantText?: string | null): string {
283+
export function formatReadyEvent(sessionId: string, lastAssistantText?: string | null, agentLabel = 'coding agent'): string {
283284
const trimmed = lastAssistantText?.trim()
284285
if (trimmed) {
285-
return `The coding agent finished working in session: ${sessionId}. Summarize this for the human immediately:\n<text>${trimmed}</text>`
286+
return `${agentLabel} finished working in session: ${sessionId}. Summarize this for the human immediately:\n<text>${trimmed}</text>`
286287
}
287-
return `The coding agent finished working in session: ${sessionId}. Use the latest agent message already present in context and summarize it for the human immediately.`
288+
return `${agentLabel} finished working in session: ${sessionId}. Use the latest agent message already present in context and summarize it for the human immediately.`
288289
}

web/src/realtime/hooks/voiceHooks.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ import {
1010
extractLastAssistantSpeakable
1111
} from './contextFormatters'
1212
import { VOICE_CONFIG } from '../voiceConfig'
13-
import type { DecryptedMessage, Session } from '@/types/api'
13+
import { getFlavorLabel, isKnownFlavor } from '@hapi/protocol'
14+
import type { DecryptedMessage, Session, SessionMetadataSummary } from '@/types/api'
1415

1516
interface SessionMetadata {
1617
summary?: { text?: string }
1718
path?: string
1819
machineId?: string
1920
}
2021

22+
function getAgentLabel(session: Session | null): string {
23+
const flavor = (session?.metadata as SessionMetadataSummary | undefined)?.flavor
24+
return isKnownFlavor(flavor) ? getFlavorLabel(flavor) : 'coding agent'
25+
}
26+
2127
// Track which sessions have been reported
2228
const shownSessions = new Set<string>()
2329
let lastFocusSession: string | null = null
@@ -65,7 +71,7 @@ function reportSession(sessionId: string) {
6571
if (!session) return
6672

6773
const messages = messagesGetter?.(sessionId) ?? []
68-
const contextUpdate = formatSessionFull(session, messages)
74+
const contextUpdate = formatSessionFull(session, messages, getAgentLabel(session))
6975
reportContextualUpdate(contextUpdate)
7076
}
7177

@@ -110,8 +116,9 @@ export const voiceHooks = {
110116
onPermissionRequested(sessionId: string, requestId: string, toolName: string, toolArgs: unknown) {
111117
if (VOICE_CONFIG.DISABLE_PERMISSION_REQUESTS) return
112118

119+
const session = sessionGetter?.(sessionId) ?? null
113120
reportSession(sessionId)
114-
reportTextUpdate(formatPermissionRequest(sessionId, requestId, toolName, toolArgs))
121+
reportTextUpdate(formatPermissionRequest(sessionId, requestId, toolName, toolArgs, getAgentLabel(session)))
115122
},
116123

117124
/**
@@ -120,8 +127,9 @@ export const voiceHooks = {
120127
onMessages(sessionId: string, messages: DecryptedMessage[]) {
121128
if (VOICE_CONFIG.DISABLE_MESSAGES) return
122129

130+
const session = sessionGetter?.(sessionId) ?? null
123131
reportSession(sessionId)
124-
reportContextualUpdate(formatNewMessages(sessionId, messages))
132+
reportContextualUpdate(formatNewMessages(sessionId, messages, getAgentLabel(session)))
125133
},
126134

127135
/**
@@ -136,7 +144,7 @@ export const voiceHooks = {
136144
const session = sessionGetter?.(sessionId) ?? null
137145
const messages = messagesGetter?.(sessionId) ?? []
138146

139-
let prompt = 'THIS IS AN ACTIVE SESSION: \n\n' + formatSessionFull(session, messages)
147+
const prompt = 'THIS IS AN ACTIVE SESSION: \n\n' + formatSessionFull(session, messages, getAgentLabel(session))
140148
shownSessions.add(sessionId)
141149

142150
return prompt
@@ -148,10 +156,11 @@ export const voiceHooks = {
148156
onReady(sessionId: string) {
149157
if (VOICE_CONFIG.DISABLE_READY_EVENTS) return
150158

159+
const session = sessionGetter?.(sessionId) ?? null
151160
reportSession(sessionId)
152161
const messages = messagesGetter?.(sessionId) ?? []
153162
const lastAssistantText = extractLastAssistantSpeakable(messages)
154-
reportTextUpdate(formatReadyEvent(sessionId, lastAssistantText))
163+
reportTextUpdate(formatReadyEvent(sessionId, lastAssistantText, getAgentLabel(session)))
155164
},
156165

157166
/**

0 commit comments

Comments
 (0)