Skip to content

Commit 8179263

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 04d3d02 commit 8179263

3 files changed

Lines changed: 128 additions & 27 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, it } from 'vitest'
2+
import type { DecryptedMessage } from '@/types/api'
3+
import {
4+
formatMessage,
5+
formatPermissionRequest,
6+
formatReadyEvent,
7+
formatNewMessages,
8+
} from './contextFormatters'
9+
10+
function msg(partial: Pick<DecryptedMessage, 'id' | 'seq' | 'content'>): DecryptedMessage {
11+
return {
12+
id: partial.id,
13+
seq: partial.seq,
14+
localId: null,
15+
content: partial.content,
16+
createdAt: 0,
17+
sessionId: 'session-1'
18+
} as DecryptedMessage
19+
}
20+
21+
describe('formatMessage — agent label', () => {
22+
it('uses the provided label for assistant text', () => {
23+
const formatted = formatMessage(
24+
msg({ id: '1', seq: 1, content: { role: 'assistant', content: 'Refactor complete.' } }),
25+
'Cursor'
26+
)
27+
expect(formatted).toContain('Cursor:')
28+
expect(formatted).toContain('Refactor complete.')
29+
expect(formatted).not.toContain('Claude Code')
30+
})
31+
32+
it('defaults to coding agent when no label is given', () => {
33+
const formatted = formatMessage(
34+
msg({ id: '1', seq: 1, content: { role: 'assistant', content: 'Done.' } })
35+
)
36+
expect(formatted).toContain('coding agent:')
37+
expect(formatted).not.toContain('Claude Code')
38+
})
39+
40+
it('uses the provided label for tool-call lines', () => {
41+
const formatted = formatMessage(
42+
msg({
43+
id: '1',
44+
seq: 1,
45+
content: {
46+
role: 'assistant',
47+
content: [{ type: 'tool_use', name: 'Bash', input: { command: 'ls' } }]
48+
}
49+
}),
50+
'Gemini'
51+
)
52+
expect(formatted).toContain('Gemini is using Bash')
53+
expect(formatted).not.toContain('Claude Code')
54+
})
55+
})
56+
57+
describe('formatPermissionRequest — agent label', () => {
58+
it('uses the provided label', () => {
59+
const result = formatPermissionRequest('sid', 'rid', 'Bash', {}, 'OpenCode')
60+
expect(result).toContain('OpenCode is requesting permission')
61+
expect(result).not.toContain('Claude Code')
62+
})
63+
64+
it('defaults to coding agent', () => {
65+
const result = formatPermissionRequest('sid', 'rid', 'Bash', {})
66+
expect(result).toContain('coding agent is requesting permission')
67+
})
68+
})
69+
70+
describe('formatReadyEvent — agent label', () => {
71+
it('uses the provided label', () => {
72+
const result = formatReadyEvent('session-1', 'Codex')
73+
expect(result).toContain('Codex done working')
74+
expect(result).not.toContain('Claude Code')
75+
})
76+
77+
it('defaults to coding agent', () => {
78+
const result = formatReadyEvent('session-1')
79+
expect(result).toContain('coding agent done working')
80+
})
81+
})
82+
83+
describe('formatNewMessages — label threads through', () => {
84+
it('uses the provided label in formatted message output', () => {
85+
const result = formatNewMessages('session-1', [
86+
msg({ id: '1', seq: 1, content: { role: 'assistant', content: 'Build succeeded.' } })
87+
], 'Cursor')
88+
expect(result).toContain('Cursor:')
89+
expect(result).not.toContain('Claude Code')
90+
})
91+
})

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,18 +92,18 @@ 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 lines: string[] = []
9697
const { role, content: wrappedContent } = unwrapRoleWrappedContent(message)
9798
const { roleOverride, content } = unwrapOutputContent(wrappedContent)
9899
const normalizedRole = roleOverride ?? role
99100

100101
if (!isContentArray(content)) {
101102
if (typeof content === 'string') {
102-
return formatPlainText(normalizedRole, content)
103+
return formatPlainText(normalizedRole, content, agentLabel)
103104
}
104105
if (isObject(content) && content.type === 'text' && typeof content.text === 'string') {
105-
return formatPlainText(normalizedRole, content.text)
106+
return formatPlainText(normalizedRole, content.text, agentLabel)
106107
}
107108
return null
108109
}
@@ -117,13 +118,13 @@ export function formatMessage(message: DecryptedMessage): string | null {
117118

118119
for (const item of content) {
119120
if (item.type === 'text' && item.text) {
120-
lines.push(formatPlainText(isAssistant ? 'assistant' : 'user', item.text))
121+
lines.push(formatPlainText(isAssistant ? 'assistant' : 'user', item.text, agentLabel))
121122
} else if (item.type === 'tool_use' && !VOICE_CONFIG.DISABLE_TOOL_CALLS) {
122123
const name = item.name || 'unknown'
123124
if (VOICE_CONFIG.LIMITED_TOOL_CALLS) {
124-
lines.push(`Claude Code is using ${name}`)
125+
lines.push(`${agentLabel} is using ${name}`)
125126
} else {
126-
lines.push(`Claude Code is using ${name} with arguments: <arguments>${JSON.stringify(item.input)}</arguments>`)
127+
lines.push(`${agentLabel} is using ${name} with arguments: <arguments>${JSON.stringify(item.input)}</arguments>`)
127128
}
128129
}
129130
}
@@ -134,34 +135,34 @@ export function formatMessage(message: DecryptedMessage): string | null {
134135
return lines.join('\n\n')
135136
}
136137

137-
export function formatNewSingleMessage(sessionId: string, message: DecryptedMessage): string | null {
138-
const formatted = formatMessage(message)
138+
export function formatNewSingleMessage(sessionId: string, message: DecryptedMessage, agentLabel = 'coding agent'): string | null {
139+
const formatted = formatMessage(message, agentLabel)
139140
if (!formatted) {
140141
return null
141142
}
142143
return 'New message in session: ' + sessionId + '\n\n' + formatted
143144
}
144145

145-
export function formatNewMessages(sessionId: string, messages: DecryptedMessage[]): string | null {
146+
export function formatNewMessages(sessionId: string, messages: DecryptedMessage[], agentLabel = 'coding agent'): string | null {
146147
const formatted = [...messages]
147148
.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0))
148-
.map(formatMessage)
149+
.map(m => formatMessage(m, agentLabel))
149150
.filter(Boolean)
150151
if (formatted.length === 0) {
151152
return null
152153
}
153154
return 'New messages in session: ' + sessionId + '\n\n' + formatted.join('\n\n')
154155
}
155156

156-
export function formatHistory(sessionId: string, messages: DecryptedMessage[]): string {
157+
export function formatHistory(sessionId: string, messages: DecryptedMessage[], agentLabel = 'coding agent'): string {
157158
const messagesToFormat = VOICE_CONFIG.MAX_HISTORY_MESSAGES > 0
158159
? messages.slice(-VOICE_CONFIG.MAX_HISTORY_MESSAGES)
159160
: messages
160-
const formatted = messagesToFormat.map(formatMessage).filter(Boolean)
161+
const formatted = messagesToFormat.map(m => formatMessage(m, agentLabel)).filter(Boolean)
161162
return 'History of messages in session: ' + sessionId + '\n\n' + formatted.join('\n\n')
162163
}
163164

164-
export function formatSessionFull(session: Session | null, messages: DecryptedMessage[]): string {
165+
export function formatSessionFull(session: Session | null, messages: DecryptedMessage[], agentLabel = 'coding agent'): string {
165166
if (!session) {
166167
return 'Session not available'
167168
}
@@ -182,7 +183,7 @@ export function formatSessionFull(session: Session | null, messages: DecryptedMe
182183

183184
lines.push('## Our interaction history so far')
184185
lines.push('')
185-
lines.push(formatHistory(session.id, messages))
186+
lines.push(formatHistory(session.id, messages, agentLabel))
186187

187188
return lines.join('\n\n')
188189
}
@@ -199,6 +200,6 @@ export function formatSessionFocus(sessionId: string, _metadata?: SessionMetadat
199200
return `Session became focused: ${sessionId}`
200201
}
201202

202-
export function formatReadyEvent(sessionId: string): string {
203-
return `Claude Code done working in session: ${sessionId}. The previous message(s) are the summary of the work done. Report this to the human immediately.`
203+
export function formatReadyEvent(sessionId: string, agentLabel = 'coding agent'): string {
204+
return `${agentLabel} done working in session: ${sessionId}. The previous message(s) are the summary of the work done. Report this to the human immediately.`
204205
}

web/src/realtime/hooks/voiceHooks.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import {
99
formatSessionOnline
1010
} from './contextFormatters'
1111
import { VOICE_CONFIG } from '../voiceConfig'
12-
import type { DecryptedMessage, Session } from '@/types/api'
12+
import { getFlavorLabel, isKnownFlavor } from '@hapi/protocol'
13+
import type { DecryptedMessage, Session, SessionMetadataSummary } from '@/types/api'
14+
15+
function getAgentLabel(session: Session | null): string {
16+
const flavor = (session?.metadata as SessionMetadataSummary | undefined)?.flavor
17+
return isKnownFlavor(flavor) ? getFlavorLabel(flavor) : 'coding agent'
18+
}
1319

1420
interface SessionMetadata {
1521
summary?: { text?: string }
@@ -64,7 +70,7 @@ function reportSession(sessionId: string) {
6470
if (!session) return
6571

6672
const messages = messagesGetter?.(sessionId) ?? []
67-
const contextUpdate = formatSessionFull(session, messages)
73+
const contextUpdate = formatSessionFull(session, messages, getAgentLabel(session))
6874
reportContextualUpdate(contextUpdate)
6975
}
7076

@@ -108,8 +114,9 @@ export const voiceHooks = {
108114
onPermissionRequested(sessionId: string, requestId: string, toolName: string, toolArgs: unknown) {
109115
if (VOICE_CONFIG.DISABLE_PERMISSION_REQUESTS) return
110116

117+
const session = sessionGetter?.(sessionId) ?? null
111118
reportSession(sessionId)
112-
reportTextUpdate(formatPermissionRequest(sessionId, requestId, toolName, toolArgs))
119+
reportTextUpdate(formatPermissionRequest(sessionId, requestId, toolName, toolArgs, getAgentLabel(session)))
113120
},
114121

115122
/**
@@ -118,8 +125,9 @@ export const voiceHooks = {
118125
onMessages(sessionId: string, messages: DecryptedMessage[]) {
119126
if (VOICE_CONFIG.DISABLE_MESSAGES) return
120127

128+
const session = sessionGetter?.(sessionId) ?? null
121129
reportSession(sessionId)
122-
reportContextualUpdate(formatNewMessages(sessionId, messages))
130+
reportContextualUpdate(formatNewMessages(sessionId, messages, getAgentLabel(session)))
123131
},
124132

125133
/**
@@ -134,20 +142,21 @@ export const voiceHooks = {
134142
const session = sessionGetter?.(sessionId) ?? null
135143
const messages = messagesGetter?.(sessionId) ?? []
136144

137-
let prompt = 'THIS IS AN ACTIVE SESSION: \n\n' + formatSessionFull(session, messages)
145+
let prompt = 'THIS IS AN ACTIVE SESSION: \n\n' + formatSessionFull(session, messages, getAgentLabel(session))
138146
shownSessions.add(sessionId)
139147

140148
return prompt
141149
},
142150

143151
/**
144-
* Called when Claude Code finishes processing (ready event)
152+
* Called when agent finishes processing (ready event)
145153
*/
146154
onReady(sessionId: string) {
147155
if (VOICE_CONFIG.DISABLE_READY_EVENTS) return
148156

157+
const session = sessionGetter?.(sessionId) ?? null
149158
reportSession(sessionId)
150-
reportTextUpdate(formatReadyEvent(sessionId))
159+
reportTextUpdate(formatReadyEvent(sessionId, getAgentLabel(session)))
151160
},
152161

153162
/**

0 commit comments

Comments
 (0)