11/**
22 * Tool card grouping — wraps consecutive same-tool calls in a single
3- * continuous container element. Groups are always expanded.
3+ * collapsible container. Groups of 3+ start collapsed; groups of 2 start
4+ * expanded (grouping with stripped card chrome only).
45 */
56
7+ /* ── Group header summary ──────────────────────────────── */
8+
9+ /** Map tool-name → human-readable plural label for the group header. */
10+ const TOOL_GROUP_LABELS : Record < string , string > = {
11+ fill_formula : "fill operations" ,
12+ write_cells : "edits" ,
13+ read_range : "reads" ,
14+ format_cells : "format operations" ,
15+ conditional_format : "conditional formats" ,
16+ view_settings : "view changes" ,
17+ chart : "chart operations" ,
18+ execute_office_js : "script executions" ,
19+ } ;
20+
21+ function describeGroup ( toolName : string , count : number ) : string {
22+ const label = TOOL_GROUP_LABELS [ toolName ] ?? `${ toolName } calls` ;
23+ return `${ count } ${ label } ` ;
24+ }
25+
26+ function buildGroupHeader ( toolName : string , count : number , collapsed : boolean ) : HTMLButtonElement {
27+ const btn = document . createElement ( "button" ) ;
28+ btn . type = "button" ;
29+ btn . className = "pi-tool-group__header" ;
30+ btn . setAttribute ( "aria-expanded" , collapsed ? "false" : "true" ) ;
31+
32+ const chevron = document . createElement ( "span" ) ;
33+ chevron . className = "pi-tool-group__chevron" ;
34+ chevron . textContent = "▸" ;
35+
36+ const label = document . createElement ( "span" ) ;
37+ label . className = "pi-tool-group__label" ;
38+ label . textContent = describeGroup ( toolName , count ) ;
39+
40+ btn . append ( chevron , label ) ;
41+
42+ btn . addEventListener ( "click" , ( ) => {
43+ const wrapper = btn . parentElement ;
44+ if ( ! wrapper ) return ;
45+ const isCollapsed = wrapper . classList . toggle ( "pi-tool-group--collapsed" ) ;
46+ btn . setAttribute ( "aria-expanded" , isCollapsed ? "false" : "true" ) ;
47+ } ) ;
48+
49+ return btn ;
50+ }
51+
52+ /* ── Collapse threshold ──────────────────────────────────── */
53+
54+ /** Groups with this many or more items start collapsed. */
55+ const COLLAPSE_THRESHOLD = 3 ;
56+
57+ /* ── Main entry point ──────────────────────────────────── */
58+
659/**
760 * Initialise tool-card grouping on the given root element.
861 * Returns a cleanup function that disconnects the observer and removes
@@ -17,6 +70,8 @@ export function initToolGrouping(root: HTMLElement): () => void {
1770 for ( const wrapper of root . querySelectorAll ( ".pi-tool-group" ) ) {
1871 const parent = wrapper . parentNode ;
1972 if ( ! parent ) continue ;
73+ // Remove injected header before unwrapping.
74+ wrapper . querySelector ( ".pi-tool-group__header" ) ?. remove ( ) ;
2075 while ( wrapper . firstChild ) parent . insertBefore ( wrapper . firstChild , wrapper ) ;
2176 parent . removeChild ( wrapper ) ;
2277 }
@@ -39,23 +94,26 @@ export function initToolGrouping(root: HTMLElement): () => void {
3994 }
4095
4196 // Identify runs of 2+ consecutive same-name completed tools.
42- const runs : Element [ ] [ ] = [ ] ;
97+ const runs : { elements : Element [ ] ; toolName : string } [ ] = [ ] ;
4398 let currentRun : Element [ ] = [ ] ;
99+ let currentToolName = "" ;
44100
45101 for ( const el of toolMessages ) {
46102 const card = el . querySelector ( ".pi-tool-card" ) ;
47103 if ( ! card ) {
48- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
104+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
49105 currentRun = [ ] ;
106+ currentToolName = "" ;
50107 continue ;
51108 }
52109
53- const toolName = card . getAttribute ( "data-tool-name" ) ;
110+ const toolName = card . getAttribute ( "data-tool-name" ) ?? "" ;
54111 const cardState = card . getAttribute ( "data-state" ) ;
55112
56113 if ( cardState !== "complete" || ! toolName ) {
57- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
114+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
58115 currentRun = [ ] ;
116+ currentToolName = "" ;
59117 continue ;
60118 }
61119
@@ -67,25 +125,33 @@ export function initToolGrouping(root: HTMLElement): () => void {
67125 if ( prevName === toolName && areConsecutiveSiblings ( prev , el ) ) {
68126 currentRun . push ( el ) ;
69127 } else {
70- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
128+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
71129 currentRun = [ el ] ;
130+ currentToolName = toolName ;
72131 }
73132 } else {
74133 currentRun . push ( el ) ;
134+ currentToolName = toolName ;
75135 }
76136 }
77- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
137+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
78138
79- // Wrap each run in a container element.
139+ // Wrap each run in a container element with a collapsible header .
80140 for ( const run of runs ) {
81- const leader = run [ 0 ] ;
82- const members = run . slice ( 1 ) ;
141+ const leader = run . elements [ 0 ] ;
142+ const members = run . elements . slice ( 1 ) ;
143+ const count = run . elements . length ;
144+ const collapsed = count >= COLLAPSE_THRESHOLD ;
83145
84146 const wrapper = document . createElement ( "div" ) ;
85- wrapper . className = "pi-tool-group" ;
147+ wrapper . className = "pi-tool-group" + ( collapsed ? " pi-tool-group--collapsed" : "" ) ;
148+
149+ // Inject group header with summary + expand/collapse toggle.
150+ const header = buildGroupHeader ( run . toolName , count , collapsed ) ;
151+ wrapper . appendChild ( header ) ;
86152
87153 if ( leader . parentNode ) leader . parentNode . insertBefore ( wrapper , leader ) ;
88- for ( const el of run ) wrapper . appendChild ( el ) ;
154+ for ( const el of run . elements ) wrapper . appendChild ( el ) ;
89155
90156 for ( const m of members ) m . classList . add ( "pi-group-member" ) ;
91157 }
0 commit comments