@@ -125,6 +125,39 @@ function makeMessage(
125125 } ;
126126}
127127
128+ function makeSession (
129+ overrides : Partial < Session > = { } ,
130+ ) : Session {
131+ return {
132+ id : "session-1" ,
133+ agent : "claude" ,
134+ project : "proj-a" ,
135+ machine : "test" ,
136+ first_message : "hello" ,
137+ started_at : "2026-02-20T12:30:00Z" ,
138+ ended_at : "2026-02-20T12:31:00Z" ,
139+ message_count : 3 ,
140+ user_message_count : 2 ,
141+ total_output_tokens : 0 ,
142+ peak_context_tokens : 0 ,
143+ is_automated : false ,
144+ created_at : "2026-02-20T12:30:00Z" ,
145+ ...overrides ,
146+ } as Session ;
147+ }
148+
149+ async function renderRole (
150+ message : MessageWithTokenFlags ,
151+ props : Record < string , unknown > = { } ,
152+ ) {
153+ const component = mount ( MessageContent , {
154+ target : document . body ,
155+ props : { message, ...props } ,
156+ } ) ;
157+ await tick ( ) ;
158+ return component ;
159+ }
160+
128161afterEach ( ( ) => {
129162 setLocale ( "en" ) ;
130163 document . body . innerHTML = "" ;
@@ -140,6 +173,183 @@ beforeEach(() => {
140173} ) ;
141174
142175describe ( "MessageContent" , ( ) => {
176+ it ( "labels inline teammate transcript messages as Teammate" , async ( ) => {
177+ const content = `Another Claude session sent a message:
178+ <teammate-message teammate_id="batch-d-browser" color="pink" summary="Batch D complete; item 9 needs delegation">
179+ Batch D (browser/picker/tabs/media-monitor) is done...
180+ </teammate-message>` ;
181+ sessionsState . sessions = [ makeSession ( ) ] ;
182+ sessionsState . activeSession = sessionsState . sessions [ 0 ] ! ;
183+
184+ const component = await renderRole (
185+ makeMessage ( {
186+ id : 10 ,
187+ role : "user" ,
188+ content,
189+ content_length : content . length ,
190+ } ) ,
191+ ) ;
192+
193+ expect ( document . querySelector ( ".role-label" ) ?. textContent ?. trim ( ) ) . toBe (
194+ "Teammate" ,
195+ ) ;
196+ expect ( document . querySelector ( ".role-icon" ) ?. textContent ?. trim ( ) ) . toBe (
197+ "T" ,
198+ ) ;
199+ unmount ( component ) ;
200+ } ) ;
201+
202+ it ( "keeps ordinary user prompts labeled as User" , async ( ) => {
203+ sessionsState . sessions = [ makeSession ( ) ] ;
204+ const component = await renderRole (
205+ makeMessage ( { id : 11 , role : "user" , content : "Please summarize this." } ) ,
206+ ) ;
207+
208+ expect ( document . querySelector ( ".role-label" ) ?. textContent ?. trim ( ) ) . toBe (
209+ "User" ,
210+ ) ;
211+ expect ( document . querySelector ( ".role-icon" ) ?. textContent ?. trim ( ) ) . toBe (
212+ "U" ,
213+ ) ;
214+ unmount ( component ) ;
215+ } ) ;
216+
217+ it ( "keeps teammate ancestry rows labeled as Teammate" , async ( ) => {
218+ sessionsState . sessions = [
219+ makeSession ( {
220+ id : "teammate-session" ,
221+ first_message : "<teammate-message>hello</teammate-message>" ,
222+ } ) ,
223+ ] ;
224+ const component = await renderRole (
225+ makeMessage ( { id : 12 , role : "user" , session_id : "teammate-session" } ) ,
226+ ) ;
227+
228+ expect ( document . querySelector ( ".role-label" ) ?. textContent ?. trim ( ) ) . toBe (
229+ "Teammate" ,
230+ ) ;
231+ expect ( document . querySelector ( ".role-icon" ) ?. textContent ?. trim ( ) ) . toBe (
232+ "T" ,
233+ ) ;
234+ unmount ( component ) ;
235+ } ) ;
236+
237+ it ( "keeps subagent ancestry rows labeled as Agent when inline teammate markup is present" , async ( ) => {
238+ const content = '<teammate-message teammate_id="batch-d-browser">reply</teammate-message>' ;
239+ sessionsState . sessions = [
240+ makeSession ( {
241+ id : "subagent-session" ,
242+ relationship_type : "subagent" ,
243+ } ) ,
244+ ] ;
245+ const component = await renderRole (
246+ makeMessage ( {
247+ id : 13 ,
248+ role : "user" ,
249+ session_id : "subagent-session" ,
250+ content,
251+ content_length : content . length ,
252+ } ) ,
253+ ) ;
254+
255+ expect ( document . querySelector ( ".role-label" ) ?. textContent ?. trim ( ) ) . toBe (
256+ "Agent" ,
257+ ) ;
258+ expect ( document . querySelector ( ".role-icon" ) ?. textContent ?. trim ( ) ) . toBe (
259+ "S" ,
260+ ) ;
261+ unmount ( component ) ;
262+ } ) ;
263+
264+ it ( "does not relabel teammate wrappers inside fenced code blocks" , async ( ) => {
265+ const content = "```xml\n<teammate-message teammate_id=\"batch-d-browser\">\nreply\n</teammate-message>\n```" ;
266+ sessionsState . sessions = [ makeSession ( ) ] ;
267+ const component = await renderRole (
268+ makeMessage ( {
269+ id : 14 ,
270+ role : "user" ,
271+ content,
272+ content_length : content . length ,
273+ } ) ,
274+ ) ;
275+
276+ expect ( document . querySelector ( ".role-label" ) ?. textContent ?. trim ( ) ) . toBe (
277+ "User" ,
278+ ) ;
279+ expect ( document . querySelector ( ".role-icon" ) ?. textContent ?. trim ( ) ) . toBe (
280+ "U" ,
281+ ) ;
282+ unmount ( component ) ;
283+ } ) ;
284+
285+ it ( "keeps inline teammate, ancestry, subagent, and code-fence rows separated" , async ( ) => {
286+ const cases = [
287+ {
288+ content : '<teammate-message teammate_id="t">reply</teammate-message>' ,
289+ session : makeSession ( ) ,
290+ props : { } ,
291+ label : "Teammate" ,
292+ icon : "T" ,
293+ } ,
294+ {
295+ content : "ordinary" ,
296+ session : makeSession ( {
297+ id : "teammate-session" ,
298+ first_message : "<teammate-message>hello</teammate-message>" ,
299+ } ) ,
300+ props : { } ,
301+ label : "Teammate" ,
302+ icon : "T" ,
303+ } ,
304+ {
305+ content : '<teammate-message teammate_id="t">reply</teammate-message>' ,
306+ session : makeSession ( {
307+ id : "subagent-session" ,
308+ relationship_type : "subagent" ,
309+ } ) ,
310+ props : { } ,
311+ label : "Agent" ,
312+ icon : "S" ,
313+ } ,
314+ {
315+ content : "ordinary" ,
316+ session : makeSession ( ) ,
317+ props : { isSubagentContext : true } ,
318+ label : "Agent" ,
319+ icon : "S" ,
320+ } ,
321+ {
322+ content : "```xml\n<teammate-message teammate_id=\"t\">reply</teammate-message>\n```" ,
323+ session : makeSession ( ) ,
324+ props : { } ,
325+ label : "User" ,
326+ icon : "U" ,
327+ } ,
328+ ] ;
329+
330+ for ( const [ index , testCase ] of cases . entries ( ) ) {
331+ document . body . innerHTML = "" ;
332+ sessionsState . sessions = [ testCase . session ] ;
333+ const component = await renderRole (
334+ makeMessage ( {
335+ id : 100 + index ,
336+ role : "user" ,
337+ session_id : testCase . session . id ,
338+ content : testCase . content ,
339+ content_length : testCase . content . length ,
340+ } ) ,
341+ testCase . props ,
342+ ) ;
343+ expect ( document . querySelector ( ".role-label" ) ?. textContent ?. trim ( ) ) . toBe (
344+ testCase . label ,
345+ ) ;
346+ expect ( document . querySelector ( ".role-icon" ) ?. textContent ?. trim ( ) ) . toBe (
347+ testCase . icon ,
348+ ) ;
349+ unmount ( component ) ;
350+ }
351+ } ) ;
352+
143353 it ( "renders message controls in Simplified Chinese without translating content" , async ( ) => {
144354 setLocale ( "zh-CN" ) ;
145355 const component = mount ( MessageContent , {
0 commit comments