@@ -17,9 +17,15 @@ import {
1717 type RunId ,
1818} from "@t3tools/contracts" ;
1919import type { ThreadRunSummary } from "@t3tools/client-runtime/state/shell" ;
20+ import {
21+ summarizeT3ToolCalls ,
22+ type T3ToolSummaryCall ,
23+ } from "@t3tools/client-runtime/t3ToolSummary" ;
2024import {
2125 resolveT3McpToolPresentation ,
26+ resolveT3McpToolSummaryAction ,
2227 type T3McpToolPresentation ,
28+ type T3McpToolSummaryAction ,
2329} from "@t3tools/shared/t3McpToolPresentation" ;
2430
2531export function workEntryIsVisibleInGroup (
@@ -360,24 +366,112 @@ function toolGroupActionLabel(action: ToolGroupAction, count: number): string {
360366 }
361367}
362368
363- /** Immediate, provider-neutral fallback while generated tool summaries are unavailable. */
364- export function summarizeToolGroup ( entries : ReadonlyArray < WorkLogEntry > ) : string {
365- const groupedEntries = new Map < ToolGroupAction , WorkLogEntry [ ] > ( ) ;
369+ function t3ToolSummaryCall ( entry : WorkLogEntry ) : T3ToolSummaryCall {
370+ const item = entry . structuredPayload ?? entry . projectedItem ?. item ;
371+ const data =
372+ entry . toolData !== null && typeof entry . toolData === "object"
373+ ? ( entry . toolData as Record < string , unknown > )
374+ : undefined ;
375+ return {
376+ input : item ?. type === "dynamic_tool" ? item . input : data ?. input ,
377+ output : item ?. type === "dynamic_tool" ? item . output : data ?. output ,
378+ // A status/read result may describe a failed child. Only the call's own lifecycle
379+ // and MCP error envelope determine whether the orchestration action failed.
380+ outcome :
381+ entry . toolLifecycleStatus === "failed" ||
382+ entry . toolLifecycleStatus === "declined" ||
383+ entry . tone === "error"
384+ ? "failed"
385+ : entry . toolLifecycleStatus === "completed"
386+ ? "completed"
387+ : "unfinished" ,
388+ } ;
389+ }
390+
391+ function summaryActionPriority ( action : ToolGroupAction | T3McpToolSummaryAction ) : number {
392+ switch ( action ) {
393+ case "command" :
394+ case "edit" :
395+ case "delegate" :
396+ case "task-cancel" :
397+ case "thread-create" :
398+ case "thread-send" :
399+ case "thread-interrupt" :
400+ case "schedule-create" :
401+ case "schedule-update" :
402+ case "schedule-delete" :
403+ return 0 ;
404+ case "other" :
405+ case "update" :
406+ return 2 ;
407+ default :
408+ return 1 ;
409+ }
410+ }
411+
412+ /** Summarizes at most two action categories; every omitted call still counts in the remainder. */
413+ export function summarizeToolGroup ( entries : ReadonlyArray < WorkLogEntry > ) : {
414+ summary : string ;
415+ hasFailure : boolean ;
416+ } {
417+ const groups = new Map <
418+ ToolGroupAction | T3McpToolSummaryAction ,
419+ {
420+ action : ToolGroupAction ;
421+ t3Action : T3McpToolSummaryAction | null ;
422+ entries : WorkLogEntry [ ] ;
423+ }
424+ > ( ) ;
366425 for ( const entry of entries ) {
426+ const item = entry . structuredPayload ?? entry . projectedItem ?. item ;
427+ const t3Action = resolveT3McpToolSummaryAction (
428+ ( item ?. type === "dynamic_tool" ? item . toolName : null ) ?? entry . toolTitle ?? entry . label ,
429+ ) ;
367430 const action = toolGroupAction ( entry ) ;
368- const group = groupedEntries . get ( action ) ;
369- if ( group ) group . push ( entry ) ;
370- else groupedEntries . set ( action , [ entry ] ) ;
431+ const key = t3Action ?? action ;
432+ const group = groups . get ( key ) ;
433+ if ( group ) group . entries . push ( entry ) ;
434+ else groups . set ( key , { action, t3Action, entries : [ entry ] } ) ;
435+ }
436+ const summaries = [ ...groups ] . map ( ( [ action , group ] , index ) => ( {
437+ index,
438+ count : group . entries . length ,
439+ priority : summaryActionPriority ( action ) ,
440+ ...( group . t3Action
441+ ? summarizeT3ToolCalls ( group . t3Action , group . entries . map ( t3ToolSummaryCall ) )
442+ : {
443+ label : toolGroupActionLabel (
444+ group . action ,
445+ toolGroupActionCount ( group . action , group . entries ) ,
446+ ) ,
447+ failedCount : group . entries . filter ( workEntryDisplayIndicatesToolFailure ) . length ,
448+ unfinishedCount : 0 ,
449+ } ) ,
450+ } ) ) ;
451+ const selected = summaries
452+ . toSorted ( ( a , b ) => a . priority - b . priority || a . index - b . index )
453+ . slice ( 0 , 2 )
454+ . sort ( ( a , b ) => a . index - b . index ) ;
455+ const labels = selected . map ( ( { label } ) => label ) ;
456+ const remainingCount = entries . length - selected . reduce ( ( count , group ) => count + group . count , 0 ) ;
457+ if ( remainingCount > 0 ) {
458+ labels . push ( `Performed ${ remainingCount } other ${ remainingCount === 1 ? "action" : "actions" } ` ) ;
371459 }
372- const labels = [ ...groupedEntries ] . map ( ( [ action , actionEntries ] ) =>
373- toolGroupActionLabel ( action , toolGroupActionCount ( action , actionEntries ) ) ,
374- ) ;
375460 const sentenceLabels = labels . map ( ( label , index ) =>
376461 index === 0 ? label : label . charAt ( 0 ) . toLowerCase ( ) + label . slice ( 1 ) ,
377462 ) ;
378- if ( sentenceLabels . length < 2 ) return sentenceLabels [ 0 ] ?? "" ;
379- if ( sentenceLabels . length === 2 ) return sentenceLabels . join ( " and " ) ;
380- return `${ sentenceLabels . slice ( 0 , - 1 ) . join ( ", " ) } , and ${ sentenceLabels . at ( - 1 ) } ` ;
463+ const summary =
464+ sentenceLabels . length < 3
465+ ? sentenceLabels . join ( " and " )
466+ : `${ sentenceLabels . slice ( 0 , - 1 ) . join ( ", " ) } , and ${ sentenceLabels . at ( - 1 ) } ` ;
467+ const failedCount = summaries . reduce ( ( count , group ) => count + group . failedCount , 0 ) ;
468+ const unfinishedCount = summaries . reduce ( ( count , group ) => count + group . unfinishedCount , 0 ) ;
469+ const statuses = [
470+ ...( failedCount > 0 ? [ `${ failedCount } failed` ] : [ ] ) ,
471+ ...( unfinishedCount > 0 ? [ `${ unfinishedCount } unfinished` ] : [ ] ) ,
472+ ] ;
473+ // Keep failure counts visible when a narrow timeline truncates the action text.
474+ return { summary : [ ...statuses , summary ] . join ( " · " ) , hasFailure : failedCount > 0 } ;
381475}
382476
383477function toolGroupSummaryKind ( entries : ReadonlyArray < WorkLogEntry > ) : ToolGroupSummaryKind {
@@ -833,7 +927,7 @@ export function deriveMessagesTimelineRows(input: {
833927 const groupId = workGroupId ( timelineEntry . id ) ;
834928 const expanded = input . expandedWorkGroupIds ?. has ( groupId ) ?? false ;
835929 const summaryKind = toolGroupSummaryKind ( visibleGroupedEntries ) ;
836- const latestToolEntry = visibleGroupedEntries . findLast ( workLogEntryIsToolLike ) ;
930+ const groupSummary = summarizeToolGroup ( visibleGroupedEntries ) ;
837931 nextRows . push ( {
838932 kind : "work-toggle" ,
839933 id : `work-toggle:${ timelineEntry . id } ` ,
@@ -845,11 +939,9 @@ export function deriveMessagesTimelineRows(input: {
845939 visibleGroupedEntries . length === 1 &&
846940 ! workLogEntryIsToolLike ( visibleGroupedEntries [ 0 ] ! )
847941 ? visibleGroupedEntries [ 0 ] ! . label
848- : summarizeToolGroup ( visibleGroupedEntries ) ,
942+ : groupSummary . summary ,
849943 summaryKind,
850- hasFailure :
851- latestToolEntry !== undefined &&
852- workEntryDisplayIndicatesToolFailure ( latestToolEntry ) ,
944+ hasFailure : groupSummary . hasFailure ,
853945 } ) ;
854946 if ( expanded ) {
855947 for ( const [ entryIndex , workEntry ] of visibleGroupedEntries . entries ( ) ) {
0 commit comments