@@ -596,7 +596,9 @@ define(function (require, exports, module) {
596596 Skill : { icon : "fa-solid fa-puzzle-piece" , color : "#e0c060" , label : Strings . AI_CHAT_TOOL_SKILL } ,
597597 "mcp__phoenix-editor__getEditorState" : { icon : "fa-solid fa-code" , color : "#6bc76b" , label : Strings . AI_CHAT_TOOL_EDITOR_STATE } ,
598598 "mcp__phoenix-editor__takeScreenshot" : { icon : "fa-solid fa-camera" , color : "#c084fc" , label : Strings . AI_CHAT_TOOL_SCREENSHOT } ,
599- "mcp__phoenix-editor__execJsInLivePreview" : { icon : "fa-solid fa-eye" , color : "#66bb6a" , label : Strings . AI_CHAT_TOOL_LIVE_PREVIEW_JS }
599+ "mcp__phoenix-editor__execJsInLivePreview" : { icon : "fa-solid fa-eye" , color : "#66bb6a" , label : Strings . AI_CHAT_TOOL_LIVE_PREVIEW_JS } ,
600+ "mcp__phoenix-editor__controlEditor" : { icon : "fa-solid fa-code" , color : "#6bc76b" , label : Strings . AI_CHAT_TOOL_CONTROL_EDITOR } ,
601+ TodoWrite : { icon : "fa-solid fa-list-check" , color : "#66bb6a" , label : Strings . AI_CHAT_TOOL_TASKS }
600602 } ;
601603
602604 function _onProgress ( _event , data ) {
@@ -637,6 +639,42 @@ define(function (require, exports, module) {
637639 }
638640 }
639641
642+ /**
643+ * Start an elapsed-time counter on a tool indicator. Called when the tool's
644+ * stale timer fires (no streaming activity for 2s).
645+ */
646+ function _startElapsedTimer ( $tool ) {
647+ if ( $tool . data ( "elapsedTimer" ) ) {
648+ return ; // already running
649+ }
650+ const startTime = $tool . data ( "startTime" ) || Date . now ( ) ;
651+ const $header = $tool . find ( ".ai-tool-header" ) ;
652+ let $elapsed = $header . find ( ".ai-tool-elapsed" ) ;
653+ if ( ! $elapsed . length ) {
654+ $elapsed = $ ( '<span class="ai-tool-elapsed"></span>' ) ;
655+ $header . append ( $elapsed ) ;
656+ }
657+ function update ( ) {
658+ const secs = Math . floor ( ( Date . now ( ) - startTime ) / 1000 ) ;
659+ if ( secs < 60 ) {
660+ $elapsed . text ( secs + "s" ) ;
661+ } else {
662+ const m = Math . floor ( secs / 60 ) ;
663+ const s = secs % 60 ;
664+ $elapsed . text ( m + "m " + ( s < 10 ? "0" : "" ) + s + "s" ) ;
665+ }
666+ }
667+ update ( ) ;
668+ const timerId = setInterval ( function ( ) {
669+ if ( $tool . hasClass ( "ai-tool-done" ) ) {
670+ clearInterval ( timerId ) ;
671+ return ;
672+ }
673+ update ( ) ;
674+ } , 1000 ) ;
675+ $tool . data ( "elapsedTimer" , timerId ) ;
676+ }
677+
640678 function _onToolStream ( _event , data ) {
641679 const uniqueToolId = ( _currentRequestId || "" ) + "-" + data . toolId ;
642680 _traceToolStreamCounts [ uniqueToolId ] = ( _traceToolStreamCounts [ uniqueToolId ] || 0 ) + 1 ;
@@ -682,6 +720,7 @@ define(function (require, exports, module) {
682720 if ( $livePreview . length && ! $tool . hasClass ( "ai-tool-done" ) ) {
683721 $livePreview . text ( phrases [ idx ] ) ;
684722 }
723+ _startElapsedTimer ( $tool ) ;
685724 _toolStreamRotateTimer = setInterval ( function ( ) {
686725 idx = ( idx + 1 ) % phrases . length ;
687726 const $p = $tool . find ( ".ai-tool-preview" ) ;
@@ -1146,6 +1185,7 @@ define(function (require, exports, module) {
11461185 $tool . find ( ".ai-tool-label" ) . text ( config . label + "..." ) ;
11471186 $tool . css ( "--tool-color" , config . color ) ;
11481187 $tool . attr ( "data-tool-icon" , config . icon ) ;
1188+ $tool . data ( "startTime" , Date . now ( ) ) ;
11491189 $messages . append ( $tool ) ;
11501190 _scrollToBottom ( ) ;
11511191 }
@@ -1173,9 +1213,38 @@ define(function (require, exports, module) {
11731213 // Update label to include summary
11741214 $tool . find ( ".ai-tool-label" ) . text ( detail . summary ) ;
11751215
1176- // For screenshot tools, add a detail container that will be populated
1177- // when the screenshot capture completes (via screenshotCaptured event)
1178- if ( toolName === "mcp__phoenix-editor__takeScreenshot" ) {
1216+ // For TodoWrite, render a mini task-list widget and auto-expand
1217+ if ( toolName === "TodoWrite" && toolInput && toolInput . todos ) {
1218+ const $detail = $ ( '<div class="ai-tool-detail"></div>' ) ;
1219+ const $todoList = $ ( '<div class="ai-todo-list"></div>' ) ;
1220+ toolInput . todos . forEach ( function ( todo ) {
1221+ let iconClass , statusClass ;
1222+ if ( todo . status === "completed" ) {
1223+ iconClass = "fa-solid fa-circle-check" ;
1224+ statusClass = "completed" ;
1225+ } else if ( todo . status === "in_progress" ) {
1226+ iconClass = "fa-solid fa-spinner fa-spin" ;
1227+ statusClass = "in_progress" ;
1228+ } else {
1229+ iconClass = "fa-regular fa-circle" ;
1230+ statusClass = "pending" ;
1231+ }
1232+ const $item = $ (
1233+ '<div class="ai-todo-item">' +
1234+ '<span class="ai-todo-icon ' + statusClass + '"><i class="' + iconClass + '"></i></span>' +
1235+ '<span class="ai-todo-content ' + ( todo . status === "completed" ? "completed" : "" ) + '"></span>' +
1236+ '</div>'
1237+ ) ;
1238+ $item . find ( ".ai-todo-content" ) . text ( todo . content ) ;
1239+ $todoList . append ( $item ) ;
1240+ } ) ;
1241+ $detail . append ( $todoList ) ;
1242+ $tool . append ( $detail ) ;
1243+ $tool . addClass ( "ai-tool-expanded" ) ;
1244+ $tool . find ( ".ai-tool-header" ) . on ( "click" , function ( ) {
1245+ $tool . toggleClass ( "ai-tool-expanded" ) ;
1246+ } ) . css ( "cursor" , "pointer" ) ;
1247+ } else if ( toolName === "mcp__phoenix-editor__takeScreenshot" ) {
11791248 const $detail = $ ( '<div class="ai-tool-detail"></div>' ) ;
11801249 $tool . append ( $detail ) ;
11811250 $tool . data ( "awaitingScreenshot" , true ) ;
@@ -1211,6 +1280,14 @@ define(function (require, exports, module) {
12111280 clearTimeout ( _toolStreamStaleTimer ) ;
12121281 clearInterval ( _toolStreamRotateTimer ) ;
12131282
1283+ // Stop the elapsed timer and remove the element
1284+ const elapsedTimer = $tool . data ( "elapsedTimer" ) ;
1285+ if ( elapsedTimer ) {
1286+ clearInterval ( elapsedTimer ) ;
1287+ $tool . removeData ( "elapsedTimer" ) ;
1288+ }
1289+ $tool . find ( ".ai-tool-elapsed" ) . remove ( ) ;
1290+
12141291 // Delay marking as done so the streaming preview stays visible briefly.
12151292 // The ai-tool-done class hides the preview via CSS; deferring it lets the
12161293 // browser paint the preview before it disappears.
@@ -1289,6 +1366,79 @@ define(function (require, exports, module) {
12891366 summary : Strings . AI_CHAT_TOOL_LIVE_PREVIEW_JS ,
12901367 lines : input . code ? input . code . split ( "\n" ) . slice ( 0 , 20 ) : [ ]
12911368 } ;
1369+ case "TodoWrite" : {
1370+ const todos = input . todos || [ ] ;
1371+ const completed = todos . filter ( function ( t ) { return t . status === "completed" ; } ) . length ;
1372+ return {
1373+ summary : StringUtils . format ( Strings . AI_CHAT_TOOL_TASKS_SUMMARY , completed , todos . length ) ,
1374+ lines : [ ]
1375+ } ;
1376+ }
1377+ case "mcp__phoenix-editor__controlEditor" : {
1378+ // Multi-operation batch format
1379+ if ( input . operations && input . operations . length ) {
1380+ if ( input . operations . length === 1 ) {
1381+ // Single operation — show its detail
1382+ const op = input . operations [ 0 ] ;
1383+ const fn = ( op . filePath || "" ) . split ( "/" ) . pop ( ) ;
1384+ let opSummary ;
1385+ switch ( op . operation ) {
1386+ case "open" :
1387+ case "openInWorkingSet" :
1388+ opSummary = "Open " + fn ;
1389+ break ;
1390+ case "close" :
1391+ opSummary = "Close " + fn ;
1392+ break ;
1393+ case "setCursorPos" :
1394+ opSummary = "Go to L" + ( op . line || "?" ) + " in " + fn ;
1395+ break ;
1396+ case "setSelection" :
1397+ opSummary = "Select L" + ( op . startLine || "?" ) + "-L" + ( op . endLine || "?" ) + " in " + fn ;
1398+ break ;
1399+ default :
1400+ opSummary = Strings . AI_CHAT_TOOL_CONTROL_EDITOR ;
1401+ }
1402+ return { summary : opSummary , lines : [ op . filePath || "" ] } ;
1403+ }
1404+ // Multiple operations — summarize
1405+ const count = input . operations . length ;
1406+ const opTypes = { } ;
1407+ input . operations . forEach ( function ( op ) {
1408+ const t = op . operation || "open" ;
1409+ opTypes [ t ] = ( opTypes [ t ] || 0 ) + 1 ;
1410+ } ) ;
1411+ const parts = Object . keys ( opTypes ) . map ( function ( t ) {
1412+ const label = ( t === "open" || t === "openInWorkingSet" ) ? "Open" :
1413+ t === "close" ? "Close" :
1414+ t === "setCursorPos" ? "Navigate" :
1415+ t === "setSelection" ? "Select" : t ;
1416+ return label + " " + opTypes [ t ] ;
1417+ } ) ;
1418+ return { summary : parts . join ( ", " ) + " files" , lines : [ ] } ;
1419+ }
1420+ // Legacy single-operation format
1421+ const fileName = ( input . filePath || "" ) . split ( "/" ) . pop ( ) ;
1422+ let opSummary ;
1423+ switch ( input . operation ) {
1424+ case "open" :
1425+ case "openInWorkingSet" :
1426+ opSummary = "Open " + fileName ;
1427+ break ;
1428+ case "close" :
1429+ opSummary = "Close " + fileName ;
1430+ break ;
1431+ case "setCursorPos" :
1432+ opSummary = "Go to L" + ( input . line || "?" ) + " in " + fileName ;
1433+ break ;
1434+ case "setSelection" :
1435+ opSummary = "Select L" + ( input . startLine || "?" ) + "-L" + ( input . endLine || "?" ) + " in " + fileName ;
1436+ break ;
1437+ default :
1438+ opSummary = Strings . AI_CHAT_TOOL_CONTROL_EDITOR ;
1439+ }
1440+ return { summary : opSummary , lines : [ input . filePath || "" ] } ;
1441+ }
12921442 default : {
12931443 // Fallback: use TOOL_CONFIG label if available
12941444 const cfg = TOOL_CONFIG [ toolName ] ;
@@ -1306,6 +1456,13 @@ define(function (require, exports, module) {
13061456 function _finishActiveTools ( ) {
13071457 $messages . find ( ".ai-msg-tool:not(.ai-tool-done)" ) . each ( function ( ) {
13081458 const $prev = $ ( this ) ;
1459+ // Clear any running elapsed timer
1460+ const et = $prev . data ( "elapsedTimer" ) ;
1461+ if ( et ) {
1462+ clearInterval ( et ) ;
1463+ $prev . removeData ( "elapsedTimer" ) ;
1464+ }
1465+ $prev . find ( ".ai-tool-elapsed" ) . remove ( ) ;
13091466 // _updateToolIndicator already ran — let the delayed timeout handle it
13101467 if ( $prev . find ( ".ai-tool-icon" ) . length ) {
13111468 return ;
0 commit comments