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
1164export function initToolGrouping ( root : HTMLElement ) : ( ) => void {
1265 let rafId = 0 ;
1366
67+ /**
68+ * Preserve user toggle state across regrouping passes. Keyed on the
69+ * first tool-message element of each group — if the user explicitly
70+ * expanded or collapsed a group, we restore that state when the group
71+ * is rebuilt rather than using the default.
72+ */
73+ const userToggleState = new WeakMap < Element , boolean > ( ) ;
74+
1475 /* ── Unwrap existing groups ────────────────────────────── */
1576
1677 function unwrapAll ( ) {
1778 for ( const wrapper of root . querySelectorAll ( ".pi-tool-group" ) ) {
1879 const parent = wrapper . parentNode ;
1980 if ( ! parent ) continue ;
81+
82+ // Snapshot user toggle state before unwrapping.
83+ const firstMessage = wrapper . querySelector ( "tool-message" ) ;
84+ if ( firstMessage ) {
85+ userToggleState . set (
86+ firstMessage ,
87+ wrapper . classList . contains ( "pi-tool-group--collapsed" ) ,
88+ ) ;
89+ }
90+
91+ // Remove injected header before unwrapping.
92+ wrapper . querySelector ( ".pi-tool-group__header" ) ?. remove ( ) ;
2093 while ( wrapper . firstChild ) parent . insertBefore ( wrapper . firstChild , wrapper ) ;
2194 parent . removeChild ( wrapper ) ;
2295 }
@@ -39,23 +112,26 @@ export function initToolGrouping(root: HTMLElement): () => void {
39112 }
40113
41114 // Identify runs of 2+ consecutive same-name completed tools.
42- const runs : Element [ ] [ ] = [ ] ;
115+ const runs : { elements : Element [ ] ; toolName : string } [ ] = [ ] ;
43116 let currentRun : Element [ ] = [ ] ;
117+ let currentToolName = "" ;
44118
45119 for ( const el of toolMessages ) {
46120 const card = el . querySelector ( ".pi-tool-card" ) ;
47121 if ( ! card ) {
48- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
122+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
49123 currentRun = [ ] ;
124+ currentToolName = "" ;
50125 continue ;
51126 }
52127
53- const toolName = card . getAttribute ( "data-tool-name" ) ;
128+ const toolName = card . getAttribute ( "data-tool-name" ) ?? "" ;
54129 const cardState = card . getAttribute ( "data-state" ) ;
55130
56131 if ( cardState !== "complete" || ! toolName ) {
57- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
132+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
58133 currentRun = [ ] ;
134+ currentToolName = "" ;
59135 continue ;
60136 }
61137
@@ -67,25 +143,36 @@ export function initToolGrouping(root: HTMLElement): () => void {
67143 if ( prevName === toolName && areConsecutiveSiblings ( prev , el ) ) {
68144 currentRun . push ( el ) ;
69145 } else {
70- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
146+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
71147 currentRun = [ el ] ;
148+ currentToolName = toolName ;
72149 }
73150 } else {
74151 currentRun . push ( el ) ;
152+ currentToolName = toolName ;
75153 }
76154 }
77- if ( currentRun . length >= 2 ) runs . push ( currentRun ) ;
155+ if ( currentRun . length >= 2 ) runs . push ( { elements : currentRun , toolName : currentToolName } ) ;
78156
79- // Wrap each run in a container element.
157+ // Wrap each run in a container element with a collapsible header .
80158 for ( const run of runs ) {
81- const leader = run [ 0 ] ;
82- const members = run . slice ( 1 ) ;
159+ const leader = run . elements [ 0 ] ;
160+ const members = run . elements . slice ( 1 ) ;
161+ const count = run . elements . length ;
162+
163+ // Restore user toggle state if available, otherwise use default.
164+ const savedState = userToggleState . get ( leader ) ;
165+ const collapsed = savedState ?? count >= COLLAPSE_THRESHOLD ;
83166
84167 const wrapper = document . createElement ( "div" ) ;
85- wrapper . className = "pi-tool-group" ;
168+ wrapper . className = "pi-tool-group" + ( collapsed ? " pi-tool-group--collapsed" : "" ) ;
169+
170+ // Inject group header with summary + expand/collapse toggle.
171+ const header = buildGroupHeader ( run . toolName , count , collapsed ) ;
172+ wrapper . appendChild ( header ) ;
86173
87174 if ( leader . parentNode ) leader . parentNode . insertBefore ( wrapper , leader ) ;
88- for ( const el of run ) wrapper . appendChild ( el ) ;
175+ for ( const el of run . elements ) wrapper . appendChild ( el ) ;
89176
90177 for ( const m of members ) m . classList . add ( "pi-group-member" ) ;
91178 }
0 commit comments