11import { escapeHtml , renderMarkdown } from './utils.js' ;
22import {
3- duplicateLogEventKey ,
43 getLogTaskGroupId ,
54 isGroupedTaskEvent ,
65 normalizeLogTs ,
7- summarizeLogEvent ,
6+ summarizeChatLiveEvent ,
87} from './log_events.js' ;
98
109const CHAT_STORAGE_KEY = 'ouro_chat' ;
@@ -94,9 +93,17 @@ export function initChat({ ws, state, updateUnreadBadge }) {
9493 liveCard . innerHTML = `
9594 <summary>
9695 <div class="chat-live-summary">
97- <span class="chat-live-phase info" data-live-phase>idle</span>
98- <span class="chat-live-title" data-live-title>Waiting for work</span>
99- <span class="chat-live-count" data-live-count hidden>0 updates</span>
96+ <div class="chat-live-summary-main">
97+ <span class="chat-live-phase working" data-live-phase>Working</span>
98+ <span class="chat-live-title" data-live-title>Waiting for work</span>
99+ </div>
100+ <div class="chat-live-summary-side">
101+ <span class="chat-live-count" data-live-count hidden>2 notes</span>
102+ <span class="chat-live-toggle" data-live-toggle>Show details</span>
103+ <svg class="chat-live-chevron" width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
104+ <path d="M5 7.5 10 12.5 15 7.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path>
105+ </svg>
106+ </div>
100107 </div>
101108 <div class="chat-live-meta" data-live-meta></div>
102109 </summary>
@@ -106,13 +113,16 @@ export function initChat({ ws, state, updateUnreadBadge }) {
106113 const liveCardTitle = liveCard . querySelector ( '[data-live-title]' ) ;
107114 const liveCardCount = liveCard . querySelector ( '[data-live-count]' ) ;
108115 const liveCardMeta = liveCard . querySelector ( '[data-live-meta]' ) ;
116+ const liveCardToggle = liveCard . querySelector ( '[data-live-toggle]' ) ;
109117 const liveCardTimeline = liveCard . querySelector ( '[data-live-timeline]' ) ;
110118 const liveCardState = {
111119 groupId : '' ,
112120 updates : 0 ,
113121 finished : false ,
114122 items : [ ] ,
123+ lastHumanHeadline : '' ,
115124 } ;
125+ liveCard . addEventListener ( 'toggle' , syncLiveCardToggle ) ;
116126
117127 function buildMessageKey ( role , text , timestamp , opts = { } ) {
118128 if ( opts . clientMessageId ) return `client|${ opts . clientMessageId } ` ;
@@ -221,15 +231,18 @@ export function initChat({ ws, state, updateUnreadBadge }) {
221231 liveCardState . updates = 0 ;
222232 liveCardState . finished = false ;
223233 liveCardState . items = [ ] ;
234+ liveCardState . lastHumanHeadline = '' ;
224235 liveCardTitle . textContent = 'Working...' ;
225- liveCardPhase . textContent = 'progress' ;
226- liveCardPhase . className = 'chat-live-phase progress' ;
236+ liveCardPhase . dataset . phase = 'working' ;
237+ liveCardPhase . textContent = 'Working' ;
238+ liveCardPhase . className = 'chat-live-phase working' ;
227239 liveCardCount . hidden = true ;
228- liveCardCount . textContent = '0 updates ' ;
240+ liveCardCount . textContent = '0 notes ' ;
229241 liveCardMeta . innerHTML = '' ;
230242 liveCardTimeline . innerHTML = '' ;
231243 liveCard . open = false ;
232244 liveCard . dataset . finished = '0' ;
245+ syncLiveCardToggle ( ) ;
233246 }
234247
235248 function ensureLiveCardVisible ( ) {
@@ -241,17 +254,27 @@ export function initChat({ ws, state, updateUnreadBadge }) {
241254 insertMessageNode ( liveCard ) ;
242255 }
243256
257+ function formatLiveCardPhaseLabel ( phase ) {
258+ if ( phase === 'thinking' ) return 'Thinking' ;
259+ if ( phase === 'working' ) return 'Working' ;
260+ if ( phase === 'done' ) return 'Done' ;
261+ if ( phase === 'error' || phase === 'timeout' ) return 'Issue' ;
262+ if ( ! phase ) return 'Working' ;
263+ return phase . charAt ( 0 ) . toUpperCase ( ) + phase . slice ( 1 ) ;
264+ }
265+
266+ function syncLiveCardToggle ( ) {
267+ if ( ! liveCardToggle ) return ;
268+ liveCardToggle . textContent = liveCard . open ? 'Hide details' : 'Show details' ;
269+ }
270+
244271 function renderLiveCardTimeline ( ) {
245272 liveCardTimeline . innerHTML = liveCardState . items . map ( ( item ) => `
246- <div class="chat-live-line">
247- <div class="chat-live-line-main">
248- <span class="chat-live-line-phase ${ item . phase || 'info' } ">${ escapeHtml ( item . phase || 'info' ) } </span>
273+ <div class="chat-live-line ${ item . phase || 'working' } ">
274+ <div class="chat-live-line-head">
249275 <span class="chat-live-line-title">${ escapeHtml ( item . headline ) } </span>
250- <span class="chat-live-line-repeat" ${ item . count > 1 ? '' : 'hidden' } >${ item . count > 1 ? `x${ item . count } ` : '' } </span>
251- </div>
252- <div class="chat-live-line-meta">
253- ${ item . ts ? `<span>${ escapeHtml ( item . ts ) } </span>` : '' }
254- ${ item . meta . map ( ( meta ) => `<span class="chat-live-pill">${ escapeHtml ( meta ) } </span>` ) . join ( '' ) }
276+ <span class="chat-live-line-repeat" ${ item . count > 1 ? '' : 'hidden' } >${ item . count > 1 ? `${ item . count } x` : '' } </span>
277+ ${ item . ts ? `<span class="chat-live-line-time">${ escapeHtml ( item . ts ) } </span>` : '' }
255278 </div>
256279 ${ item . body ? `<div class="chat-live-line-body">${ escapeHtml ( item . body ) } </div>` : '' }
257280 </div>
@@ -270,34 +293,52 @@ export function initChat({ ws, state, updateUnreadBadge }) {
270293 liveCardState . updates += 1 ;
271294 liveCardState . finished = [ 'done' , 'error' , 'timeout' ] . includes ( summary . phase || '' ) ;
272295 liveCard . dataset . finished = liveCardState . finished ? '1' : '0' ;
273- liveCardPhase . textContent = summary . phase || 'info' ;
274- liveCardPhase . className = `chat-live-phase ${ summary . phase || 'info' } ` ;
275- liveCardTitle . textContent = summary . headline || 'Working...' ;
276- liveCardCount . hidden = false ;
277- liveCardCount . textContent = `${ liveCardState . updates } updates` ;
278- liveCardMeta . innerHTML = [
279- nextGroupId === 'bg-consciousness' ? 'background' : `task=${ nextGroupId } ` ,
280- ts || '' ,
281- ...summary . meta ,
282- ] . filter ( Boolean ) . map ( ( item ) => `<span class="chat-live-pill">${ escapeHtml ( item ) } </span>` ) . join ( '' ) ;
283-
284- const last = liveCardState . items [ liveCardState . items . length - 1 ] ;
285- const syntheticKey = dedupeKey || `${ summary . phase || 'info' } |${ summary . headline || '' } |${ summary . body || '' } ` ;
286- if ( last && last . dedupeKey === syntheticKey ) {
287- last . count += 1 ;
288- last . ts = ts || last . ts ;
289- } else {
290- liveCardState . items . push ( {
291- phase : summary . phase || 'info' ,
292- headline : summary . headline || 'Update' ,
293- body : summary . body || '' ,
294- meta : summary . meta || [ ] ,
295- ts : ts || '' ,
296- count : 1 ,
297- dedupeKey : syntheticKey ,
298- } ) ;
299- if ( liveCardState . items . length > 20 ) liveCardState . items . shift ( ) ;
296+ const headline = summary . headline || 'Working...' ;
297+ if ( summary . human && headline ) {
298+ liveCardState . lastHumanHeadline = headline ;
299+ }
300+
301+ const shouldPromote =
302+ Boolean ( summary . promote )
303+ || ! liveCardState . lastHumanHeadline
304+ || liveCardState . finished ;
305+ const activeHeadline = shouldPromote
306+ ? headline
307+ : ( liveCardState . lastHumanHeadline || headline ) ;
308+ const activePhase = liveCardState . finished
309+ ? ( summary . phase || 'done' )
310+ : ( shouldPromote ? ( summary . phase || 'working' ) : ( liveCardPhase . dataset . phase || 'working' ) ) ;
311+
312+ liveCardPhase . dataset . phase = activePhase ;
313+ liveCardPhase . textContent = formatLiveCardPhaseLabel ( activePhase ) ;
314+ liveCardPhase . className = `chat-live-phase ${ activePhase } ` ;
315+ liveCardTitle . textContent = activeHeadline ;
316+
317+ const syntheticKey = summary . dedupeKey || dedupeKey || `${ summary . phase || 'working' } |${ headline } |${ summary . body || '' } ` ;
318+ const shouldRenderLine = summary . visible !== false && Boolean ( headline || summary . body ) ;
319+ if ( shouldRenderLine ) {
320+ const last = liveCardState . items [ liveCardState . items . length - 1 ] ;
321+ if ( last && last . dedupeKey === syntheticKey ) {
322+ last . count += 1 ;
323+ last . ts = ts || last . ts ;
324+ } else {
325+ liveCardState . items . push ( {
326+ phase : summary . phase || 'working' ,
327+ headline : headline || 'Update' ,
328+ body : summary . body || '' ,
329+ ts : ts || '' ,
330+ count : 1 ,
331+ dedupeKey : syntheticKey ,
332+ } ) ;
333+ if ( liveCardState . items . length > 20 ) liveCardState . items . shift ( ) ;
334+ }
300335 }
336+ liveCardCount . hidden = liveCardState . items . length < 2 ;
337+ liveCardCount . textContent = `${ liveCardState . items . length } notes` ;
338+ liveCardMeta . innerHTML = [
339+ nextGroupId === 'bg-consciousness' ? 'Background thinking' : '' ,
340+ ts ? `Latest ${ ts } ` : '' ,
341+ ] . filter ( Boolean ) . map ( ( item ) => `<span class="chat-live-meta-text">${ escapeHtml ( item ) } </span>` ) . join ( '' ) ;
301342 renderLiveCardTimeline ( ) ;
302343 insertMessageNode ( liveCard ) ;
303344 hideTypingIndicatorOnly ( ) ;
@@ -309,32 +350,31 @@ export function initChat({ ws, state, updateUnreadBadge }) {
309350 }
310351
311352 function updateLiveCardFromProgressMessage ( msg ) {
312- const content = String ( msg ?. content || '' ) . replace ( / ^ 💬 \s * / , '' ) . trim ( ) ;
313- if ( ! content ) return ;
353+ const summary = summarizeChatLiveEvent ( {
354+ type : 'send_message' ,
355+ is_progress : true ,
356+ content : msg ?. content || '' ,
357+ text : msg ?. content || '' ,
358+ task_id : liveCardState . groupId || 'chat' ,
359+ } ) ;
360+ if ( ! summary ) return ;
314361 applyLiveCardState (
315- {
316- phase : 'progress' ,
317- headline : content ,
318- body : '' ,
319- meta : [ 'thought' ] ,
320- } ,
362+ summary ,
321363 liveCardState . groupId || 'chat' ,
322364 normalizeLogTs ( msg . ts || new Date ( ) . toISOString ( ) ) ,
323- `progress: ${ content } ` ,
365+ summary . dedupeKey || '' ,
324366 ) ;
325367 }
326368
327369 function updateLiveCardFromLogEvent ( evt ) {
328370 if ( ! evt || ! isGroupedTaskEvent ( evt ) ) return ;
329- const summary = summarizeLogEvent ( evt ) ;
330- const dedupeKey = evt . type === 'send_message'
331- ? `progress:${ summary . headline || '' } `
332- : ( duplicateLogEventKey ( evt ) || `${ evt . type || evt . event || 'event' } |${ summary . phase || '' } |${ summary . headline || '' } ` ) ;
371+ const summary = summarizeChatLiveEvent ( evt ) ;
372+ if ( ! summary ) return ;
333373 applyLiveCardState (
334374 summary ,
335375 getLogTaskGroupId ( evt ) || liveCardState . groupId || 'active' ,
336376 normalizeLogTs ( evt . ts || evt . timestamp ) ,
337- dedupeKey ,
377+ summary . dedupeKey || '' ,
338378 ) ;
339379 }
340380
@@ -621,8 +661,9 @@ export function initChat({ ws, state, updateUnreadBadge }) {
621661 if ( liveCardState . groupId && ! liveCardState . finished ) {
622662 liveCardState . finished = true ;
623663 liveCard . dataset . finished = '1' ;
624- if ( liveCardPhase . textContent === 'progress' || liveCardPhase . textContent === 'calling' ) {
625- liveCardPhase . textContent = 'done' ;
664+ if ( [ 'working' , 'thinking' ] . includes ( liveCardPhase . dataset . phase || '' ) ) {
665+ liveCardPhase . dataset . phase = 'done' ;
666+ liveCardPhase . textContent = 'Done' ;
626667 liveCardPhase . className = 'chat-live-phase done' ;
627668 }
628669 }
0 commit comments