9595 # messages { padding : 6px 0 20px ; }
9696 .msg { margin : 14px 0 ; padding : 12px 16px ; border-radius : 12px ; white-space : pre-wrap; word-break : break-word; max-width : 85% ; }
9797 .msg .user { background : var (--accent-soft ); color : var (--fg ); margin-left : auto; border : 1px solid var (--line ); }
98- .msg .agent { background : var ( --panel ) ; border : 1 px solid var ( --line ) ; box-shadow : var ( --shadow ) ; }
98+ .msg .agent { background : transparent ; border : 0 ; box-shadow : none; max-width : 100 % ; padding : 2 px 0 ; }
9999 .msg .tool { background : transparent; border : 1px dashed var (--line ); color : var (--muted ); font-family : ui-monospace, monospace; font-size : 12px ; max-width : 100% ; }
100100 .msg .err { background : transparent; border : 1px solid var (--err ); color : var (--err ); max-width : 100% ; }
101101 .msg .who { font-size : 12px ; color : var (--muted ); margin-bottom : 4px ; }
102102 .msg pre { background : rgba (0 , 0 , 0 , .05 ); border-radius : 8px ; padding : 10px ; overflow : auto; font-size : 12px ; }
103+ .progress { display : flex; align-items : center; gap : 8px ; margin : 10px 0 ; font-size : 13px ; color : var (--muted ); }
104+ .progress .spin { display : inline-block; animation : spin 1s linear infinite; }
103105 .artifact { display : inline-flex; align-items : center; gap : 6px ; background : var (--panel ); border : 1px solid var (--line ); border-radius : 999px ; padding : 5px 14px ; font-size : 13px ; text-decoration : none; color : var (--fg ); box-shadow : var (--shadow ); }
104106 .artifact : hover { border-color : var (--accent ); color : var (--accent ); }
105107 .artifact .size { color : var (--muted ); font-size : 11px ; }
@@ -334,7 +336,12 @@ <h1>一句话处理 Excel 表格</h1>
334336 const box = $ ( 'messages' ) ; box . innerHTML = '' ;
335337 for ( const run of detail . runs ) {
336338 addMsg ( 'user' , run . prompt ) ;
337- if ( run . answer ) addMsg ( 'agent' , run . answer ) ;
339+ if ( run . events && run . events . length ) {
340+ let agentDiv = null ;
341+ for ( const e of run . events ) agentDiv = renderEvent ( e , agentDiv , false ) ;
342+ } else if ( run . answer ) {
343+ addMsg ( 'agent' , run . answer ) ;
344+ }
338345 if ( run . error ) addMsg ( 'err' , run . error ) ;
339346 }
340347 loadConversations ( ) ;
@@ -432,7 +439,6 @@ <h1>一句话处理 Excel 表格</h1>
432439 const div = document . createElement ( 'div' ) ;
433440 div . className = 'msg ' + kind ;
434441 if ( kind === 'agent' ) {
435- div . innerHTML = `<div class="who">AI</div>` ;
436442 div . appendChild ( renderAnswer ( text ) ) ;
437443 } else {
438444 div . textContent = text ;
@@ -441,6 +447,29 @@ <h1>一句话处理 Excel 表格</h1>
441447 $ ( 'messages' ) . scrollTop = $ ( 'messages' ) . scrollHeight ;
442448 return div ;
443449}
450+ function addAgentText ( ) {
451+ const div = document . createElement ( 'div' ) ;
452+ div . className = 'msg agent' ;
453+ const span = document . createElement ( 'span' ) ;
454+ div . appendChild ( span ) ;
455+ $ ( 'messages' ) . appendChild ( div ) ;
456+ $ ( 'messages' ) . scrollTop = $ ( 'messages' ) . scrollHeight ;
457+ return span ;
458+ }
459+ // One live progress row per tool: tool_running updates it in place,
460+ // tool_start (a new tool / message boundary) starts a fresh row.
461+ let liveProgress = null ;
462+ function addProgress ( text , busy , fresh = false ) {
463+ if ( ! liveProgress || fresh ) {
464+ const div = document . createElement ( 'div' ) ;
465+ div . className = 'progress' ;
466+ $ ( 'messages' ) . appendChild ( div ) ;
467+ liveProgress = div ;
468+ }
469+ liveProgress . innerHTML = ( busy ? '<span class="spin">⚙️</span>' : '<span>✅</span>' ) +
470+ '<span>' + escapeHtml ( text ) + '</span>' ;
471+ $ ( 'messages' ) . scrollTop = $ ( 'messages' ) . scrollHeight ;
472+ }
444473function renderAnswer ( text ) {
445474 // minimal markdown: code fences + bold
446475 const parts = String ( text ) . split ( / ` ` ` / ) ;
@@ -511,6 +540,86 @@ <h1>一句话处理 Excel 表格</h1>
511540 if ( Array . isArray ( d ) ) return d . map ( strData ) . join ( ', ' ) ;
512541 return JSON . stringify ( d ) ;
513542}
543+ // The harness's "tool_calls" payload -> "Bash(command='ls -la')".
544+ // Each entry is {name, args}; args values arrive pre-formatted and
545+ // pre-truncated by the harness (repr strings), so they are safe to show
546+ // verbatim -- addProgress escapes them before they reach the DOM.
547+ function toolCallLabel ( calls ) {
548+ if ( ! Array . isArray ( calls ) ) return '' ;
549+ return calls . map ( c => toolOneLabel ( c ) ) . filter ( Boolean ) . join ( ', ' ) ;
550+ }
551+ function toolOneLabel ( c ) {
552+ const name = ( c && c . name ) || 'tool' ;
553+ const args = ( c && typeof c . args === 'object' && c . args ) || { } ;
554+ const params = Object . keys ( args ) . map ( k => k + '=' + args [ k ] ) . join ( ' ' ) ;
555+ return params ? name + '(' + params + ')' : name ;
556+ }
557+ // A tool round reuses ONE progress row (tool_start opens it,
558+ // tool_running/tool then update it in place), so the command has to be
559+ // remembered for the row's later states -- otherwise the finished row
560+ // would read a bare "工具执行完成" and the command would be lost from the
561+ // transcript. Reset per round by tool_start.
562+ let roundLabel = '' ;
563+ let roundArgsByName = { } ;
564+ // Harness log lines that are bookkeeping rather than progress: the
565+ // session's generated title says nothing about what the agent is doing.
566+ const LOG_SKIP = [ / ^ s e s s i o n t i t l e d \b / ] ;
567+ // One harness event (live SSE or stored replay) -> message-flow DOM.
568+ // Returns the current agent-text span, or null when the event starts a
569+ // new assistant message (tool_start / retry are message boundaries).
570+ // live=false (history replay) skips ask/confirm: the run is over, so
571+ // the answer box would be dead.
572+ function renderEvent ( e , agentDiv , live = true ) {
573+ if ( e . type === 'delta' ) {
574+ if ( ! agentDiv ) agentDiv = addAgentText ( ) ;
575+ agentDiv . textContent += ( e . data ?. text ?? '' ) ;
576+ $ ( 'messages' ) . scrollTop = $ ( 'messages' ) . scrollHeight ;
577+ } else if ( e . type === 'notify' && e . data ) {
578+ const kind = e . data . kind , data = e . data . data ;
579+ if ( kind === 'tool_start' ) {
580+ agentDiv = null ;
581+ roundLabel = strData ( data ) ; // names only until tool_calls lands
582+ roundArgsByName = { } ;
583+ addProgress ( '准备执行: ' + roundLabel , true , true ) ;
584+ } else if ( kind === 'tool_calls' ) {
585+ // Arrives right behind tool_start, naming the same round but with
586+ // arguments: upgrade that row in place (no `fresh`) so the user
587+ // sees the actual command instead of a bare tool name.
588+ const label = toolCallLabel ( data ) ;
589+ if ( label ) {
590+ roundLabel = label ;
591+ for ( const c of data ) {
592+ if ( c && c . name ) roundArgsByName [ c . name ] = toolOneLabel ( c ) ;
593+ }
594+ addProgress ( '准备执行: ' + label , true ) ;
595+ }
596+ } else if ( kind === 'tool_running' ) {
597+ const name = strData ( data ) ;
598+ addProgress ( '正在执行: ' + ( roundArgsByName [ name ] || name ) , true ) ;
599+ } else if ( kind === 'tool' ) {
600+ addProgress ( roundLabel ? '完成: ' + roundLabel : '工具执行完成' , false ) ;
601+ } else if ( kind === 'todos' ) {
602+ renderTodos ( data ) ;
603+ } else if ( kind === 'error' ) {
604+ addMsg ( 'err' , typeof data === 'string' ? data : JSON . stringify ( data ) ) ;
605+ } else if ( kind === 'retry' ) {
606+ agentDiv = null ;
607+ addProgress ( '模型调用重试中…' , true , true ) ;
608+ } else if ( kind === 'compact' ) {
609+ addProgress ( '正在压缩上下文…' , true ) ;
610+ } else if ( live && ( kind === 'ask' || kind === 'confirm' ) ) {
611+ addAsk ( data , e . run_id || currentRun ) ;
612+ }
613+ } else if ( e . type === 'log' && e . data && e . data . message ) {
614+ const msg = String ( e . data . message ) ;
615+ // Own row (`fresh`): a log line would otherwise overwrite the live
616+ // tool row and erase the command the user just saw. Harness logs
617+ // are rare and noteworthy (compaction, MCP, auto-save/agent errors),
618+ // so one row each is proportionate.
619+ if ( ! LOG_SKIP . some ( re => re . test ( msg ) ) ) addProgress ( msg , true , true ) ;
620+ }
621+ return agentDiv ;
622+ }
514623function setRunStatus ( text , busy ) {
515624 const box = $ ( 'runStatus' ) ;
516625 if ( ! text ) { box . style . display = 'none' ; return ; }
@@ -538,32 +647,8 @@ <h1>一句话处理 Excel 表格</h1>
538647 let agentDiv = null ;
539648 es . onmessage = ev => {
540649 const e = JSON . parse ( ev . data ) ;
541- if ( e . type === 'delta' ) {
542- if ( ! agentDiv ) agentDiv = addMsg ( 'agent' , '' ) ;
543- agentDiv . lastChild . textContent = ( agentDiv . lastChild . textContent || '' ) + ( e . data ?. text ?? '' ) ;
544- $ ( 'messages' ) . scrollTop = $ ( 'messages' ) . scrollHeight ;
545- } else if ( e . type === 'notify' && e . data ) {
546- const kind = e . data . kind , data = e . data . data ;
547- if ( kind === 'tool_start' ) {
548- setRunStatus ( '准备执行: ' + strData ( data ) , true ) ;
549- } else if ( kind === 'tool_running' ) {
550- setRunStatus ( '正在执行: ' + strData ( data ) , true ) ;
551- } else if ( kind === 'tool' ) {
552- setRunStatus ( '工具执行完成' , true ) ;
553- } else if ( kind === 'todos' ) {
554- renderTodos ( data ) ;
555- } else if ( kind === 'error' ) {
556- addMsg ( 'err' , typeof data === 'string' ? data : JSON . stringify ( data ) ) ;
557- } else if ( kind === 'retry' ) {
558- setRunStatus ( '模型调用重试中…' , true ) ;
559- } else if ( kind === 'compact' ) {
560- setRunStatus ( '正在压缩上下文…' , true ) ;
561- } else if ( kind === 'ask' || kind === 'confirm' ) {
562- addAsk ( data , runId ) ;
563- }
564- } else if ( e . type === 'log' && e . data && e . data . message ) {
565- setRunStatus ( e . data . message , true ) ;
566- } else if ( e . type === 'run' ) {
650+ agentDiv = renderEvent ( e , agentDiv ) ;
651+ if ( e . type === 'run' ) {
567652 es . close ( ) ; es = null ; currentRun = null ;
568653 $ ( 'stopBtn2' ) . style . display = 'none' ;
569654 setRunStatus ( '' , false ) ;
0 commit comments