diff --git a/claude_code_log/html/templates/components/timeline.html b/claude_code_log/html/templates/components/timeline.html index b9daa018..3e7b38db 100644 --- a/claude_code_log/html/templates/components/timeline.html +++ b/claude_code_log/html/templates/components/timeline.html @@ -350,11 +350,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] diff --git a/claude_code_log/html/templates/transcript.html b/claude_code_log/html/templates/transcript.html index bf43d6ca..0dd26d3d 100644 --- a/claude_code_log/html/templates/transcript.html +++ b/claude_code_log/html/templates/transcript.html @@ -729,12 +729,22 @@

🔍 Search & Filter

closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -750,36 +760,6 @@

🔍 Search & Filter

} } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -788,18 +768,13 @@

🔍 Search & Filter

.filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -849,11 +824,11 @@

🔍 Search & Filter

} function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -875,52 +850,6 @@

🔍 Search & Filter

} } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index f3d159d6..b55cc8ff 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -4986,11 +4986,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -7145,12 +7145,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -7166,36 +7176,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -7204,18 +7184,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -7265,11 +7240,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -7291,52 +7266,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -13751,11 +13680,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -15805,12 +15734,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -15826,36 +15765,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -15864,18 +15773,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -15925,11 +15829,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -15951,52 +15855,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -32542,11 +32400,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -34697,12 +34555,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -34718,36 +34586,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -34756,18 +34594,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -34817,11 +34650,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -34843,52 +34676,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -48385,11 +48172,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -51107,12 +50894,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -51128,36 +50925,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -51166,18 +50933,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -51227,11 +50989,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -51253,52 +51015,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -72869,11 +72585,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -75140,12 +74856,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -75161,36 +74887,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -75199,18 +74895,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -75260,11 +74951,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -75286,52 +74977,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -84570,11 +84215,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -86847,12 +86492,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -86868,36 +86523,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -86906,18 +86531,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -86967,11 +86587,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -86993,52 +86613,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -93453,11 +93027,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -96010,12 +95584,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -96031,36 +95615,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -96069,18 +95623,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -96130,11 +95679,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -96156,52 +95705,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -102616,11 +102119,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -105120,12 +104623,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -105141,36 +104654,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -105179,18 +104662,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -105240,11 +104718,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -105266,52 +104744,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -111726,11 +111158,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -114175,12 +113607,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -114196,36 +113638,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -114234,18 +113646,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -114295,11 +113702,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -114321,52 +113728,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -120781,11 +120142,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -123058,12 +122419,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -123079,36 +122450,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -123117,18 +122458,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -123178,11 +122514,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -123204,52 +122540,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -129664,11 +128954,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -131746,12 +131036,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -131767,36 +131067,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -131805,18 +131075,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -131866,11 +131131,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -131892,52 +131157,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { @@ -138352,11 +137571,11 @@ return { timelineItems, timelineGroups, timelineEnd, timelineStart }; } - // The timeline groups a filter toggle governs. Mirrors the toggle - // expansion in the transcript's applyFilter: the single "Tool" toggle - // stands for both tool groups, and "User" also covers the + // The timeline groups a filter toggle governs: the single "Tool" + // toggle stands for both tool groups, and "User" also covers the // user-initiated bash lines. Every other toggle governs the group of - // its own name. + // its own name. The transcript's applyFilter expands toggles through + // this same map (exported below as window.timelineToggleGroups). const TOGGLE_GROUPS = { 'tool': ['tool_use', 'tool_result'], 'user': ['user', 'bash-input', 'bash-output'] @@ -140383,12 +139602,22 @@ closeToolbarButton.addEventListener('click', toggleFilterToolbar); } + // The cards a filter toggle covers, through the same toggle map + // as applyFilter: "User" also counts the bash lines, "Tool" both + // tool types. + function toggleCardSelector(type, extra = '') { + const toggleGroups = window.timelineToggleGroups; + return (toggleGroups[type] || [type]) + .map(t => `.message.${t}:not(.session-header)${extra}`) + .join(', '); + } + // Count messages by type and update button labels function updateMessageCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const messages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const messages = document.querySelectorAll(toggleCardSelector(type)); const count = messages.length; const toggle = document.querySelector(`[data-type="${type}"]`); const countSpan = toggle ? toggle.querySelector('.count') : null; @@ -140404,36 +139633,6 @@ } } }); - - // Handle combined "user" filter (user + bash-input + bash-output) - const userMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const userCount = userMessages.length; - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan) { - userCountSpan.textContent = `(${userCount})`; - if (userCount === 0) { - userToggle.style.display = 'none'; - } else { - userToggle.style.display = 'flex'; - } - } - - // Handle combined "tool" filter (tool_use + tool_result) - const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const toolCount = toolMessages.length; - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan) { - toolCountSpan.textContent = `(${toolCount})`; - if (toolCount === 0) { - toolToggle.style.display = 'none'; - } else { - toolToggle.style.display = 'flex'; - } - } } // Filter functionality @@ -140442,18 +139641,13 @@ .filter(toggle => toggle.classList.contains('active')) .map(toggle => toggle.dataset.type); - // Expand filter types to their corresponding CSS classes - const expandedTypes = []; - activeTypes.forEach(type => { - if (type === 'tool') { - expandedTypes.push('tool_use', 'tool_result'); - } else if (type === 'user') { - // User filter includes bash commands (user-initiated) - expandedTypes.push('user', 'bash-input', 'bash-output'); - } else { - expandedTypes.push(type); - } - }); + // Expand filter types to their corresponding CSS classes, + // through the map the timeline filters its groups by + // (timeline.html), so the two cannot disagree: "Tool" stands + // for both tool types, "User" also covers the user-initiated + // bash lines, and every other toggle its own type. + const toggleGroups = window.timelineToggleGroups; + const expandedTypes = activeTypes.flatMap(type => toggleGroups[type] || [type]); // Show/hide messages based on active toggle buttons const allMessages = document.querySelectorAll('.message:not(.session-header)'); @@ -140503,11 +139697,11 @@ } function updateVisibleCounts() { - const messageTypes = ['assistant', 'sidechain', 'memory', 'system', 'thinking', 'image']; + const messageTypes = ['user', 'system', 'assistant', 'thinking', 'tool', 'sidechain', 'memory', 'image']; messageTypes.forEach(type => { - const visibleMessages = document.querySelectorAll(`.message.${type}:not(.session-header):not(.filtered-hidden)`); - const totalMessages = document.querySelectorAll(`.message.${type}:not(.session-header)`); + const visibleMessages = document.querySelectorAll(toggleCardSelector(type, ':not(.filtered-hidden)')); + const totalMessages = document.querySelectorAll(toggleCardSelector(type)); const visibleCount = visibleMessages.length; const totalCount = totalMessages.length; @@ -140529,52 +139723,6 @@ } } }); - - // Handle combined "user" filter separately (includes bash messages) - const visibleUserMessages = document.querySelectorAll(`.message.user:not(.session-header):not(.filtered-hidden), .message.bash-input:not(.session-header):not(.filtered-hidden), .message.bash-output:not(.session-header):not(.filtered-hidden)`); - const totalUserMessages = document.querySelectorAll(`.message.user:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`); - const visibleUserCount = visibleUserMessages.length; - const totalUserCount = totalUserMessages.length; - - const userToggle = document.querySelector(`[data-type="user"]`); - const userCountSpan = userToggle ? userToggle.querySelector('.count') : null; - - if (userCountSpan && totalUserCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleUserCount !== totalUserCount) { - userCountSpan.textContent = `(${visibleUserCount}/${totalUserCount})`; - } else { - userCountSpan.textContent = `(${totalUserCount})`; - } - } - - // Handle combined "tool" filter separately - const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result:not(.session-header):not(.filtered-hidden)`); - const totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`); - const visibleToolCount = visibleToolMessages.length; - const totalToolCount = totalToolMessages.length; - - const toolToggle = document.querySelector(`[data-type="tool"]`); - const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null; - - if (toolCountSpan && totalToolCount > 0) { - const activeTypes = Array.from(filterToggles) - .filter(toggle => toggle.classList.contains('active')) - .map(toggle => toggle.dataset.type); - - const isFiltering = activeTypes.length < filterToggles.length; - - if (isFiltering && visibleToolCount !== totalToolCount) { - toolCountSpan.textContent = `(${visibleToolCount}/${totalToolCount})`; - } else { - toolCountSpan.textContent = `(${totalToolCount})`; - } - } } function toggleFilter(button) { diff --git a/test/test_timeline_browser.py b/test/test_timeline_browser.py index 0b616aee..ba16794b 100644 --- a/test/test_timeline_browser.py +++ b/test/test_timeline_browser.py @@ -52,6 +52,76 @@ "() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))" ) +# Rewrites the toggle map in place as timeline.html exports it, before the +# timeline or the page filter reads it: tool_result moves from the "Tool" +# toggle to the "Assistant" toggle. +_MOVE_TOOL_RESULT_TO_ASSISTANT = """ +Object.defineProperty(window, 'timelineToggleGroups', { + configurable: true, + get() { return this.__toggleGroups; }, + set(map) { + map.tool = ['tool_use']; + map.assistant = ['assistant', 'tool_result']; + this.__toggleGroups = map; + }, +}); +""" + +# Each timeline item is shown iff its card is: its group is on the timeline +# and visible, and the item is not hidden one by one. Counts the items +# compared per group, so a check that compared nothing reads as such. +_PAGE_AND_TIMELINE_AGREE = """() => { + const tl = window.timeline; + const shownGroups = new Set(tl.groupsData.get() + .filter(g => g.visible !== false).map(g => g.id)); + const cards = [...document.querySelectorAll('.message:not(.session-header)')]; + const problems = []; + const compared = {}; + tl.itemsData.get().forEach(item => { + const el = cards[item.id]; + if (!el) { problems.push('item without a card: ' + item.id); return; } + const cardShown = !el.classList.contains('filtered-hidden') + && !el.classList.contains('search-hidden'); + const itemShown = shownGroups.has(item.group) + && !(item.className || '').split(/\\s+/).includes('timeline-filtered-hidden'); + compared[item.group] = (compared[item.group] || 0) + 1; + if (cardShown !== itemShown) { + problems.push((cardShown ? 'page shows, timeline hides ' : 'page hides, timeline shows ') + + el.id + ' (' + item.group + ')'); + } + }); + return { problems, compared }; +}""" + +# Each visible toggle's count, as the toggle map says it should read: the +# cards whose type is one of the toggle's groups, as "(visible/total)" while +# a toggle is off and some of them are hidden, else "(total)". A toggle with +# no cards is hidden. Returns the totals and labels too, so a check that +# counted nothing reads as such. +_TOGGLE_COUNTS_FOLLOW_THE_MAP = """() => { + const groupsOf = window.timelineToggleGroups; + const toggles = [...document.querySelectorAll('.filter-toggle')]; + const filtering = toggles.some(t => !t.classList.contains('active')); + const cards = [...document.querySelectorAll('.message:not(.session-header)')]; + const problems = [], totals = {}, labels = {}; + toggles.forEach(toggle => { + const type = toggle.dataset.type; + const groups = groupsOf[type] || [type]; + const mine = cards.filter(el => groups.some(g => el.classList.contains(g))); + const visible = mine.filter(el => !el.classList.contains('filtered-hidden')).length; + totals[type] = mine.length; + labels[type] = toggle.querySelector('.count').textContent; + if (!mine.length) { + if (toggle.style.display !== 'none') problems.push(type + ': shown with no cards'); + return; + } + const want = filtering && visible !== mine.length + ? '(' + visible + '/' + mine.length + ')' : '(' + mine.length + ')'; + if (labels[type] !== want) problems.push(type + ': ' + labels[type] + ', want ' + want); + }); + return { problems, totals, labels }; +}""" + def _wait_for_filters(page: Page) -> None: """After a filter toggle (or Select All/None) click: the timeline's groups @@ -946,6 +1016,107 @@ def test_tool_filter_removes_tool_items_from_timeline(self, page: Page, theme: s assert page.evaluate(count_items, tool_kinds) == tools_before assert page.evaluate(count_items, side_kinds) == side_before + def _open_with_tool_result_under_assistant(self, page: Page, theme: str) -> None: + """Open representative_messages.jsonl with the toggle map rewritten + (``_MOVE_TOOL_RESULT_TO_ASSISTANT``), and check the rewrite landed.""" + messages = load_transcript(Path("test/test_data/representative_messages.jsonl")) + with tempfile.NamedTemporaryFile( + suffix=".html", delete=False, dir=self.temp_dir + ) as f: + temp_file = Path(f.name) + html = generate_html( + messages, "Toggle Map", theme=theme, output_dir=temp_file.parent + ) + temp_file.write_text(html, encoding="utf-8") + self.temp_files.append(temp_file) + + page.add_init_script(_MOVE_TOOL_RESULT_TO_ASSISTANT) + page.goto(f"file://{temp_file}") + # The rewrite reached the map the timeline and the page read. + assert page.evaluate( + "() => [window.timelineToggleGroups.tool, window.timelineToggleGroups.assistant]" + ) == [["tool_use"], ["assistant", "tool_result"]] + + @pytest.mark.browser + @pytest.mark.parametrize("theme", ["classic", "minimal"]) + def test_page_filter_expands_toggles_through_the_timelines_map( + self, page: Page, theme: str + ): + """The page filter and the timeline read one toggle map (#395). + + The map is rewritten as the page exports it, before either reads it: + tool_result moves from the "Tool" toggle to the "Assistant" toggle. + With Assistant off and Tool on, the timeline hides the tool_result + group; a page filter with a copy of its own would still show the + tool_result cards, and the two would disagree. + """ + self._open_with_tool_result_under_assistant(page, theme) + page.locator("#toggleTimeline").click() + self._wait_for_timeline_loaded(page) + _wait_for_timeline_groups(page) + + moved_cards = "#transcript .message.tool_result:not(.sidechain)" + assert page.locator(moved_cards).count() > 0 + + page.locator("#filterMessages").click() + page.locator('.filter-toggle[data-type="assistant"]').click() + _wait_for_filters(page) + + # The page and the timeline agree, item by item, the moved group + # included. + agreement = page.evaluate(_PAGE_AND_TIMELINE_AGREE) + assert agreement["problems"] == [], agreement["problems"][:10] + assert agreement["compared"].get("tool_result", 0) > 0, agreement + assert agreement["compared"].get("tool_use", 0) > 0, agreement + # And the page follows the map: no Tool toggle covers tool_result now. + assert set( + page.evaluate( + "(sel) => [...document.querySelectorAll(sel)]" + ".map(m => m.classList.contains('filtered-hidden'))", + moved_cards, + ) + ) == {True} + + @pytest.mark.browser + @pytest.mark.parametrize("theme", ["classic", "minimal"]) + def test_toggle_counts_follow_the_timelines_map(self, page: Page, theme: str): + """The toggles' counts read the same toggle map as the filter (#395). + + With tool_result moved from the "Tool" toggle to the "Assistant" + toggle (as in the test above), Tool counts only the tool_use cards + and Assistant counts the tool_result cards too, both when the page + loads and as "(visible/total)" once Assistant is turned off. + """ + self._open_with_tool_result_under_assistant(page, theme) + moved = page.locator("#transcript .message.tool_result").count() + assert moved > 0 + + counts = page.evaluate(_TOGGLE_COUNTS_FOLLOW_THE_MAP) + assert counts["problems"] == [], counts + assert counts["totals"]["tool"] > 0, counts + assert counts["totals"]["assistant"] > moved, counts + + page.locator("#filterMessages").click() + page.locator('.filter-toggle[data-type="assistant"]').click() + _wait_for_filters(page) + + counts = page.evaluate(_TOGGLE_COUNTS_FOLLOW_THE_MAP) + assert counts["problems"] == [], counts + # The visible/total form was exercised, not only the totals. + assert "/" in counts["labels"]["assistant"], counts + + # With Assistant off, the moved cards are hidden whichever map the + # visible count reads. With Assistant on and Tool off they are shown, + # so a visible count that bypassed the map would count them under + # Tool (and miss them under Assistant). + page.locator('.filter-toggle[data-type="assistant"]').click() + page.locator('.filter-toggle[data-type="tool"]').click() + _wait_for_filters(page) + + counts = page.evaluate(_TOGGLE_COUNTS_FOLLOW_THE_MAP) + assert counts["problems"] == [], counts + assert "/" in counts["labels"]["tool"], counts + @pytest.mark.browser def test_timeline_filter_individual_message_types(self, page: Page): """Test filtering individual message types in timeline."""