đ 5 transcript files
diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr
index 897b51c5..ed1796fb 100644
--- a/test/__snapshots__/test_snapshot_html.ambr
+++ b/test/__snapshots__/test_snapshot_html.ambr
@@ -2233,11 +2233,11 @@
}
.fold-bar[data-border-color="bash-input"] .fold-bar-section {
- border-bottom-color: var(--tool-use-color);
+ border-bottom-color: var(--user-color);
}
.fold-bar[data-border-color="bash-output"] .fold-bar-section {
- border-bottom-color: var(--success-dimmed);
+ border-bottom-color: var(--user-dimmed);
}
.fold-bar[data-border-color="session-header"] .fold-bar-section {
@@ -2261,7 +2261,9 @@
/* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */
.user:not(.compacted),
- .system {
+ .system,
+ .bash-input,
+ .bash-output {
margin-left: 33%;
margin-right: 0;
}
@@ -2423,6 +2425,59 @@
font-size: 80%;
}
+ /* Hook summary styling */
+ .system-hook {
+ border-left-color: var(--warning-dimmed);
+ background-color: var(--highlight-dimmed);
+ font-size: 90%;
+ }
+
+ .hook-summary {
+ cursor: pointer;
+ }
+
+ .hook-summary summary {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ }
+
+ .hook-details {
+ margin-top: 0.5em;
+ padding: 0.5em;
+ background-color: var(--code-bg);
+ border-radius: 4px;
+ }
+
+ .hook-commands {
+ margin-bottom: 0.5em;
+ }
+
+ .hook-commands code {
+ display: block;
+ padding: 0.25em 0.5em;
+ font-size: 0.85em;
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
+
+ .hook-errors {
+ margin-top: 0.5em;
+ }
+
+ .hook-error {
+ margin: 0.25em 0;
+ padding: 0.5em;
+ background-color: var(--error-semi);
+ border-left: 3px solid var(--system-error-color);
+ font-size: 0.85em;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-x: auto;
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
/* Command output styling */
.command-output {
background-color: #1e1e1e11;
@@ -2453,14 +2508,14 @@
padding: 0 1em;
}
- /* Bash command styling */
+ /* Bash command styling (user-initiated, right-aligned) */
.bash-input {
- background-color: #1e1e1e08;
- border-left-color: var(--tool-use-color);
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-color);
}
.bash-prompt {
- color: var(--tool-use-color);
+ color: var(--user-color);
font-weight: bold;
font-size: 1.1em;
margin-right: 8px;
@@ -2475,40 +2530,36 @@
border-radius: 3px;
}
- /* Bash output styling */
+ /* Bash output styling (user-initiated, right-aligned) */
.bash-output {
- background-color: var(--neutral-dimmed);
- border-left-color: #607d8b;
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-dimmed);
}
- .bash-stdout {
- background-color: #1e1e1e05;
- padding: 12px;
+ .bash-output pre.bash-stdout,
+ .bash-output pre.bash-stderr {
+ padding: 8px;
border-radius: 4px;
- border: 1px solid #00000011;
- margin: 8px 0;
+ margin: 4px 0;
font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
- color: var(--text-primary);
+ font-size: 80%;
+ line-height: 1.3;
+ white-space: pre;
overflow-x: auto;
+ overflow-y: auto;
+ max-height: 300px;
+ }
+
+ .bash-output pre.bash-stdout {
+ background-color: #1e1e1e05;
+ border: 1px solid #00000011;
+ color: var(--text-primary);
}
- .bash-stderr {
+ .bash-output pre.bash-stderr {
background-color: #ffebee;
- padding: 12px;
- border-radius: 4px;
border: 1px solid #ffcdd2;
- margin: 8px 0;
- font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
color: #c62828;
- overflow-x: auto;
}
.bash-empty {
@@ -2838,9 +2889,10 @@
margin-top: 4px;
}
- /* Tool use/result preview content with gradient fade */
+ /* Tool use/result/bash-output preview content with gradient fade */
.tool_use .preview-content,
- .tool_result .preview-content {
+ .tool_result .preview-content,
+ .bash-output .preview-content {
opacity: 0.7;
mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
@@ -5317,8 +5369,8 @@
}
});
- // Handle combined "tool" filter (tool_use + tool_result)
- const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`);
+ // Handle combined "tool" filter (tool_use + tool_result + bash messages)
+ const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const toolCount = toolMessages.length;
const toolToggle = document.querySelector(`[data-type="tool"]`);
const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null;
@@ -5339,11 +5391,11 @@
.filter(toggle => toggle.classList.contains('active'))
.map(toggle => toggle.dataset.type);
- // Expand "tool" to include both tool_use and tool_result
+ // Expand "tool" to include tool_use, tool_result, and bash messages
const expandedTypes = [];
activeTypes.forEach(type => {
if (type === 'tool') {
- expandedTypes.push('tool_use', 'tool_result');
+ expandedTypes.push('tool_use', 'tool_result', 'bash-input', 'bash-output');
} else {
expandedTypes.push(type);
}
@@ -5414,9 +5466,9 @@
}
});
- // 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)`);
+ // Handle combined "tool" filter separately (includes bash messages)
+ const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result: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 totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const visibleToolCount = visibleToolMessages.length;
const totalToolCount = totalToolMessages.length;
@@ -6912,11 +6964,11 @@
}
.fold-bar[data-border-color="bash-input"] .fold-bar-section {
- border-bottom-color: var(--tool-use-color);
+ border-bottom-color: var(--user-color);
}
.fold-bar[data-border-color="bash-output"] .fold-bar-section {
- border-bottom-color: var(--success-dimmed);
+ border-bottom-color: var(--user-dimmed);
}
.fold-bar[data-border-color="session-header"] .fold-bar-section {
@@ -6940,7 +6992,9 @@
/* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */
.user:not(.compacted),
- .system {
+ .system,
+ .bash-input,
+ .bash-output {
margin-left: 33%;
margin-right: 0;
}
@@ -7102,6 +7156,59 @@
font-size: 80%;
}
+ /* Hook summary styling */
+ .system-hook {
+ border-left-color: var(--warning-dimmed);
+ background-color: var(--highlight-dimmed);
+ font-size: 90%;
+ }
+
+ .hook-summary {
+ cursor: pointer;
+ }
+
+ .hook-summary summary {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ }
+
+ .hook-details {
+ margin-top: 0.5em;
+ padding: 0.5em;
+ background-color: var(--code-bg);
+ border-radius: 4px;
+ }
+
+ .hook-commands {
+ margin-bottom: 0.5em;
+ }
+
+ .hook-commands code {
+ display: block;
+ padding: 0.25em 0.5em;
+ font-size: 0.85em;
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
+
+ .hook-errors {
+ margin-top: 0.5em;
+ }
+
+ .hook-error {
+ margin: 0.25em 0;
+ padding: 0.5em;
+ background-color: var(--error-semi);
+ border-left: 3px solid var(--system-error-color);
+ font-size: 0.85em;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-x: auto;
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
/* Command output styling */
.command-output {
background-color: #1e1e1e11;
@@ -7132,14 +7239,14 @@
padding: 0 1em;
}
- /* Bash command styling */
+ /* Bash command styling (user-initiated, right-aligned) */
.bash-input {
- background-color: #1e1e1e08;
- border-left-color: var(--tool-use-color);
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-color);
}
.bash-prompt {
- color: var(--tool-use-color);
+ color: var(--user-color);
font-weight: bold;
font-size: 1.1em;
margin-right: 8px;
@@ -7154,40 +7261,36 @@
border-radius: 3px;
}
- /* Bash output styling */
+ /* Bash output styling (user-initiated, right-aligned) */
.bash-output {
- background-color: var(--neutral-dimmed);
- border-left-color: #607d8b;
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-dimmed);
}
- .bash-stdout {
- background-color: #1e1e1e05;
- padding: 12px;
+ .bash-output pre.bash-stdout,
+ .bash-output pre.bash-stderr {
+ padding: 8px;
border-radius: 4px;
- border: 1px solid #00000011;
- margin: 8px 0;
+ margin: 4px 0;
font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
- color: var(--text-primary);
+ font-size: 80%;
+ line-height: 1.3;
+ white-space: pre;
overflow-x: auto;
+ overflow-y: auto;
+ max-height: 300px;
+ }
+
+ .bash-output pre.bash-stdout {
+ background-color: #1e1e1e05;
+ border: 1px solid #00000011;
+ color: var(--text-primary);
}
- .bash-stderr {
+ .bash-output pre.bash-stderr {
background-color: #ffebee;
- padding: 12px;
- border-radius: 4px;
border: 1px solid #ffcdd2;
- margin: 8px 0;
- font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
color: #c62828;
- overflow-x: auto;
}
.bash-empty {
@@ -7517,9 +7620,10 @@
margin-top: 4px;
}
- /* Tool use/result preview content with gradient fade */
+ /* Tool use/result/bash-output preview content with gradient fade */
.tool_use .preview-content,
- .tool_result .preview-content {
+ .tool_result .preview-content,
+ .bash-output .preview-content {
opacity: 0.7;
mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
@@ -9441,57 +9545,6 @@
-
-
-
-
-
Sessions
-
â Click a box below to scroll down to the corresponding session
-
-
-
-
-
-
-
@@ -10154,8 +10207,8 @@
}
});
- // Handle combined "tool" filter (tool_use + tool_result)
- const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`);
+ // Handle combined "tool" filter (tool_use + tool_result + bash messages)
+ const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const toolCount = toolMessages.length;
const toolToggle = document.querySelector(`[data-type="tool"]`);
const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null;
@@ -10176,11 +10229,11 @@
.filter(toggle => toggle.classList.contains('active'))
.map(toggle => toggle.dataset.type);
- // Expand "tool" to include both tool_use and tool_result
+ // Expand "tool" to include tool_use, tool_result, and bash messages
const expandedTypes = [];
activeTypes.forEach(type => {
if (type === 'tool') {
- expandedTypes.push('tool_use', 'tool_result');
+ expandedTypes.push('tool_use', 'tool_result', 'bash-input', 'bash-output');
} else {
expandedTypes.push(type);
}
@@ -10251,9 +10304,9 @@
}
});
- // 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)`);
+ // Handle combined "tool" filter separately (includes bash messages)
+ const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result: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 totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const visibleToolCount = visibleToolMessages.length;
const totalToolCount = totalToolMessages.length;
@@ -11749,11 +11802,11 @@
}
.fold-bar[data-border-color="bash-input"] .fold-bar-section {
- border-bottom-color: var(--tool-use-color);
+ border-bottom-color: var(--user-color);
}
.fold-bar[data-border-color="bash-output"] .fold-bar-section {
- border-bottom-color: var(--success-dimmed);
+ border-bottom-color: var(--user-dimmed);
}
.fold-bar[data-border-color="session-header"] .fold-bar-section {
@@ -11777,7 +11830,9 @@
/* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */
.user:not(.compacted),
- .system {
+ .system,
+ .bash-input,
+ .bash-output {
margin-left: 33%;
margin-right: 0;
}
@@ -11939,6 +11994,59 @@
font-size: 80%;
}
+ /* Hook summary styling */
+ .system-hook {
+ border-left-color: var(--warning-dimmed);
+ background-color: var(--highlight-dimmed);
+ font-size: 90%;
+ }
+
+ .hook-summary {
+ cursor: pointer;
+ }
+
+ .hook-summary summary {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ }
+
+ .hook-details {
+ margin-top: 0.5em;
+ padding: 0.5em;
+ background-color: var(--code-bg);
+ border-radius: 4px;
+ }
+
+ .hook-commands {
+ margin-bottom: 0.5em;
+ }
+
+ .hook-commands code {
+ display: block;
+ padding: 0.25em 0.5em;
+ font-size: 0.85em;
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
+
+ .hook-errors {
+ margin-top: 0.5em;
+ }
+
+ .hook-error {
+ margin: 0.25em 0;
+ padding: 0.5em;
+ background-color: var(--error-semi);
+ border-left: 3px solid var(--system-error-color);
+ font-size: 0.85em;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-x: auto;
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
/* Command output styling */
.command-output {
background-color: #1e1e1e11;
@@ -11969,14 +12077,14 @@
padding: 0 1em;
}
- /* Bash command styling */
+ /* Bash command styling (user-initiated, right-aligned) */
.bash-input {
- background-color: #1e1e1e08;
- border-left-color: var(--tool-use-color);
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-color);
}
.bash-prompt {
- color: var(--tool-use-color);
+ color: var(--user-color);
font-weight: bold;
font-size: 1.1em;
margin-right: 8px;
@@ -11991,40 +12099,36 @@
border-radius: 3px;
}
- /* Bash output styling */
+ /* Bash output styling (user-initiated, right-aligned) */
.bash-output {
- background-color: var(--neutral-dimmed);
- border-left-color: #607d8b;
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-dimmed);
}
- .bash-stdout {
- background-color: #1e1e1e05;
- padding: 12px;
+ .bash-output pre.bash-stdout,
+ .bash-output pre.bash-stderr {
+ padding: 8px;
border-radius: 4px;
- border: 1px solid #00000011;
- margin: 8px 0;
+ margin: 4px 0;
font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
- color: var(--text-primary);
+ font-size: 80%;
+ line-height: 1.3;
+ white-space: pre;
overflow-x: auto;
+ overflow-y: auto;
+ max-height: 300px;
+ }
+
+ .bash-output pre.bash-stdout {
+ background-color: #1e1e1e05;
+ border: 1px solid #00000011;
+ color: var(--text-primary);
}
- .bash-stderr {
+ .bash-output pre.bash-stderr {
background-color: #ffebee;
- padding: 12px;
- border-radius: 4px;
border: 1px solid #ffcdd2;
- margin: 8px 0;
- font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
color: #c62828;
- overflow-x: auto;
}
.bash-empty {
@@ -12354,9 +12458,10 @@
margin-top: 4px;
}
- /* Tool use/result preview content with gradient fade */
+ /* Tool use/result/bash-output preview content with gradient fade */
.tool_use .preview-content,
- .tool_result .preview-content {
+ .tool_result .preview-content,
+ .bash-output .preview-content {
opacity: 0.7;
mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
@@ -14971,8 +15076,8 @@
}
});
- // Handle combined "tool" filter (tool_use + tool_result)
- const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`);
+ // Handle combined "tool" filter (tool_use + tool_result + bash messages)
+ const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const toolCount = toolMessages.length;
const toolToggle = document.querySelector(`[data-type="tool"]`);
const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null;
@@ -14993,11 +15098,11 @@
.filter(toggle => toggle.classList.contains('active'))
.map(toggle => toggle.dataset.type);
- // Expand "tool" to include both tool_use and tool_result
+ // Expand "tool" to include tool_use, tool_result, and bash messages
const expandedTypes = [];
activeTypes.forEach(type => {
if (type === 'tool') {
- expandedTypes.push('tool_use', 'tool_result');
+ expandedTypes.push('tool_use', 'tool_result', 'bash-input', 'bash-output');
} else {
expandedTypes.push(type);
}
@@ -15068,9 +15173,9 @@
}
});
- // 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)`);
+ // Handle combined "tool" filter separately (includes bash messages)
+ const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result: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 totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const visibleToolCount = visibleToolMessages.length;
const totalToolCount = totalToolMessages.length;
@@ -16566,11 +16671,11 @@
}
.fold-bar[data-border-color="bash-input"] .fold-bar-section {
- border-bottom-color: var(--tool-use-color);
+ border-bottom-color: var(--user-color);
}
.fold-bar[data-border-color="bash-output"] .fold-bar-section {
- border-bottom-color: var(--success-dimmed);
+ border-bottom-color: var(--user-dimmed);
}
.fold-bar[data-border-color="session-header"] .fold-bar-section {
@@ -16594,7 +16699,9 @@
/* Right-aligned messages (user-initiated, right margin 0, left margin 33%) */
.user:not(.compacted),
- .system {
+ .system,
+ .bash-input,
+ .bash-output {
margin-left: 33%;
margin-right: 0;
}
@@ -16756,6 +16863,59 @@
font-size: 80%;
}
+ /* Hook summary styling */
+ .system-hook {
+ border-left-color: var(--warning-dimmed);
+ background-color: var(--highlight-dimmed);
+ font-size: 90%;
+ }
+
+ .hook-summary {
+ cursor: pointer;
+ }
+
+ .hook-summary summary {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ }
+
+ .hook-details {
+ margin-top: 0.5em;
+ padding: 0.5em;
+ background-color: var(--code-bg);
+ border-radius: 4px;
+ }
+
+ .hook-commands {
+ margin-bottom: 0.5em;
+ }
+
+ .hook-commands code {
+ display: block;
+ padding: 0.25em 0.5em;
+ font-size: 0.85em;
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
+
+ .hook-errors {
+ margin-top: 0.5em;
+ }
+
+ .hook-error {
+ margin: 0.25em 0;
+ padding: 0.5em;
+ background-color: var(--error-semi);
+ border-left: 3px solid var(--system-error-color);
+ font-size: 0.85em;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-x: auto;
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
/* Command output styling */
.command-output {
background-color: #1e1e1e11;
@@ -16786,14 +16946,14 @@
padding: 0 1em;
}
- /* Bash command styling */
+ /* Bash command styling (user-initiated, right-aligned) */
.bash-input {
- background-color: #1e1e1e08;
- border-left-color: var(--tool-use-color);
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-color);
}
.bash-prompt {
- color: var(--tool-use-color);
+ color: var(--user-color);
font-weight: bold;
font-size: 1.1em;
margin-right: 8px;
@@ -16808,40 +16968,36 @@
border-radius: 3px;
}
- /* Bash output styling */
+ /* Bash output styling (user-initiated, right-aligned) */
.bash-output {
- background-color: var(--neutral-dimmed);
- border-left-color: #607d8b;
+ background-color: var(--highlight-light);
+ border-left-color: var(--user-dimmed);
}
- .bash-stdout {
- background-color: #1e1e1e05;
- padding: 12px;
+ .bash-output pre.bash-stdout,
+ .bash-output pre.bash-stderr {
+ padding: 8px;
border-radius: 4px;
- border: 1px solid #00000011;
- margin: 8px 0;
+ margin: 4px 0;
font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
- color: var(--text-primary);
+ font-size: 80%;
+ line-height: 1.3;
+ white-space: pre;
overflow-x: auto;
+ overflow-y: auto;
+ max-height: 300px;
+ }
+
+ .bash-output pre.bash-stdout {
+ background-color: #1e1e1e05;
+ border: 1px solid #00000011;
+ color: var(--text-primary);
}
- .bash-stderr {
+ .bash-output pre.bash-stderr {
background-color: #ffebee;
- padding: 12px;
- border-radius: 4px;
border: 1px solid #ffcdd2;
- margin: 8px 0;
- font-family: var(--font-monospace);
- font-size: 0.9em;
- line-height: 1.4;
- white-space: pre-wrap;
- word-wrap: break-word;
color: #c62828;
- overflow-x: auto;
}
.bash-empty {
@@ -17171,9 +17327,10 @@
margin-top: 4px;
}
- /* Tool use/result preview content with gradient fade */
+ /* Tool use/result/bash-output preview content with gradient fade */
.tool_use .preview-content,
- .tool_result .preview-content {
+ .tool_result .preview-content,
+ .bash-output .preview-content {
opacity: 0.7;
mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, black 80%, transparent 100%);
@@ -19650,8 +19807,8 @@
}
});
- // Handle combined "tool" filter (tool_use + tool_result)
- const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header)`);
+ // Handle combined "tool" filter (tool_use + tool_result + bash messages)
+ const toolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const toolCount = toolMessages.length;
const toolToggle = document.querySelector(`[data-type="tool"]`);
const toolCountSpan = toolToggle ? toolToggle.querySelector('.count') : null;
@@ -19672,11 +19829,11 @@
.filter(toggle => toggle.classList.contains('active'))
.map(toggle => toggle.dataset.type);
- // Expand "tool" to include both tool_use and tool_result
+ // Expand "tool" to include tool_use, tool_result, and bash messages
const expandedTypes = [];
activeTypes.forEach(type => {
if (type === 'tool') {
- expandedTypes.push('tool_use', 'tool_result');
+ expandedTypes.push('tool_use', 'tool_result', 'bash-input', 'bash-output');
} else {
expandedTypes.push(type);
}
@@ -19747,9 +19904,9 @@
}
});
- // 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)`);
+ // Handle combined "tool" filter separately (includes bash messages)
+ const visibleToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header):not(.filtered-hidden), .message.tool_result: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 totalToolMessages = document.querySelectorAll(`.message.tool_use:not(.session-header), .message.tool_result:not(.session-header), .message.bash-input:not(.session-header), .message.bash-output:not(.session-header)`);
const visibleToolCount = visibleToolMessages.length;
const totalToolCount = totalToolMessages.length;
diff --git a/test/test_hook_summary.py b/test/test_hook_summary.py
new file mode 100644
index 00000000..ee62df7c
--- /dev/null
+++ b/test/test_hook_summary.py
@@ -0,0 +1,249 @@
+"""Tests for hook summary (stop_hook_summary) parsing and rendering."""
+
+from claude_code_log.models import parse_transcript_entry, SystemTranscriptEntry
+from claude_code_log.renderer import generate_html
+
+
+class TestHookSummaryParsing:
+ """Test parsing of stop_hook_summary system entries."""
+
+ def test_parse_hook_summary_without_content(self):
+ """Test that hook summary without content field parses successfully."""
+ data = {
+ "parentUuid": "test-parent",
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "subtype": "stop_hook_summary",
+ "hookCount": 1,
+ "hookInfos": [{"command": "uv run ruff format && uv run ruff check"}],
+ "hookErrors": [],
+ "preventedContinuation": False,
+ "stopReason": "",
+ "hasOutput": False,
+ "level": "suggestion",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+
+ entry = parse_transcript_entry(data)
+
+ assert isinstance(entry, SystemTranscriptEntry)
+ assert entry.subtype == "stop_hook_summary"
+ assert entry.content is None
+ assert entry.hasOutput is False
+ assert entry.hookErrors == []
+ assert entry.hookInfos == [
+ {"command": "uv run ruff format && uv run ruff check"}
+ ]
+ assert entry.preventedContinuation is False
+
+ def test_parse_hook_summary_with_errors(self):
+ """Test that hook summary with errors parses successfully."""
+ data = {
+ "parentUuid": "test-parent",
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "subtype": "stop_hook_summary",
+ "hookCount": 1,
+ "hookInfos": [{"command": "pnpm lint"}],
+ "hookErrors": [
+ "Error: TypeScript compilation failed\nTS2307: Cannot find module"
+ ],
+ "preventedContinuation": False,
+ "stopReason": "",
+ "hasOutput": True,
+ "level": "suggestion",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+
+ entry = parse_transcript_entry(data)
+
+ assert isinstance(entry, SystemTranscriptEntry)
+ assert entry.subtype == "stop_hook_summary"
+ assert entry.hasOutput is True
+ assert entry.hookErrors is not None
+ assert len(entry.hookErrors) == 1
+ assert "TypeScript compilation failed" in entry.hookErrors[0]
+
+ def test_parse_system_message_with_content_still_works(self):
+ """Test that regular system messages with content still parse correctly."""
+ data = {
+ "parentUuid": "test-parent",
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "content": "
init",
+ "level": "info",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+
+ entry = parse_transcript_entry(data)
+
+ assert isinstance(entry, SystemTranscriptEntry)
+ assert entry.content == "
init"
+ assert entry.subtype is None
+
+
+class TestHookSummaryRendering:
+ """Test rendering of stop_hook_summary system entries."""
+
+ def test_silent_hook_success_not_rendered(self):
+ """Test that silent hook successes (no output, no errors) are not rendered."""
+ messages = [
+ {
+ "parentUuid": None,
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "subtype": "stop_hook_summary",
+ "hookCount": 1,
+ "hookInfos": [{"command": "uv run ruff format"}],
+ "hookErrors": [],
+ "preventedContinuation": False,
+ "hasOutput": False,
+ "level": "suggestion",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+ ]
+
+ parsed_messages = [parse_transcript_entry(msg) for msg in messages]
+ html = generate_html(parsed_messages)
+
+ # Should not contain actual hook content (skipped)
+ # Note: CSS class definitions for .hook-summary will still be in the HTML
+ assert "Hook failed" not in html
+ assert "Hook output" not in html
+ assert "uv run ruff format" not in html # The hook command should not appear
+
+ def test_hook_with_errors_rendered(self):
+ """Test that hooks with errors are rendered as collapsible details."""
+ messages = [
+ {
+ "parentUuid": None,
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "subtype": "stop_hook_summary",
+ "hookCount": 1,
+ "hookInfos": [{"command": "pnpm lint"}],
+ "hookErrors": ["Error: lint failed"],
+ "preventedContinuation": False,
+ "hasOutput": True,
+ "level": "suggestion",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+ ]
+
+ parsed_messages = [parse_transcript_entry(msg) for msg in messages]
+ html = generate_html(parsed_messages)
+
+ # Should contain hook summary elements
+ assert "hook-summary" in html
+ assert "Hook failed" in html
+ assert "pnpm lint" in html
+ assert "Error: lint failed" in html
+
+ def test_hook_with_output_but_no_errors_rendered(self):
+ """Test that hooks with output but no errors are rendered."""
+ messages = [
+ {
+ "parentUuid": None,
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "subtype": "stop_hook_summary",
+ "hookCount": 1,
+ "hookInfos": [{"command": "echo 'formatted'"}],
+ "hookErrors": [],
+ "preventedContinuation": False,
+ "hasOutput": True,
+ "level": "suggestion",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+ ]
+
+ parsed_messages = [parse_transcript_entry(msg) for msg in messages]
+ html = generate_html(parsed_messages)
+
+ # Should contain hook summary elements
+ assert "hook-summary" in html
+ assert "Hook output" in html # Not "Hook failed" since no errors
+
+ def test_hook_with_ansi_errors_rendered(self):
+ """Test that ANSI codes in hook errors are converted to HTML."""
+ messages = [
+ {
+ "parentUuid": None,
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "subtype": "stop_hook_summary",
+ "hookCount": 1,
+ "hookInfos": [{"command": "pnpm lint"}],
+ "hookErrors": ["\x1b[31mError:\x1b[0m Something went wrong"],
+ "preventedContinuation": False,
+ "hasOutput": True,
+ "level": "suggestion",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+ ]
+
+ parsed_messages = [parse_transcript_entry(msg) for msg in messages]
+ html = generate_html(parsed_messages)
+
+ # ANSI codes should be converted, not present raw
+ assert "\x1b[" not in html
+ assert "Something went wrong" in html
+
+ def test_regular_system_message_still_renders(self):
+ """Test that regular system messages with content still render correctly."""
+ messages = [
+ {
+ "parentUuid": None,
+ "isSidechain": False,
+ "userType": "external",
+ "cwd": "/home/user",
+ "sessionId": "test-session",
+ "version": "2.0.56",
+ "type": "system",
+ "content": "
init",
+ "level": "info",
+ "timestamp": "2025-12-02T23:05:58.427Z",
+ "uuid": "test-uuid",
+ }
+ ]
+
+ parsed_messages = [parse_transcript_entry(msg) for msg in messages]
+ html = generate_html(parsed_messages)
+
+ # Should render the command name
+ assert "init" in html
diff --git a/test/test_utils.py b/test/test_utils.py
index ec018f6c..2404862a 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -11,8 +11,20 @@
should_skip_message,
should_use_as_session_starter,
extract_text_content_length,
+ create_session_preview,
+ is_warmup_only_session,
+ get_warmup_session_ids,
+ _compact_ide_tags_for_preview,
+ FIRST_USER_MESSAGE_PREVIEW_LENGTH,
+)
+from claude_code_log.models import (
+ TextContent,
+ ToolUseContent,
+ UserTranscriptEntry,
+ UserMessage,
+ AssistantTranscriptEntry,
+ AssistantMessage,
)
-from claude_code_log.models import TextContent, ToolUseContent
class TestSystemMessageDetection:
@@ -361,3 +373,409 @@ def test_session_starter_edge_cases(self):
# Test with init in the middle of command name
init_middle = "
reinitReinitialize"
assert should_use_as_session_starter(init_middle) is False
+
+
+class TestWarmupMessageFiltering:
+ """Test warmup message filtering in session starters."""
+
+ def test_should_not_use_warmup_as_starter(self):
+ """Test that 'Warmup' messages are filtered out from session starters."""
+ assert should_use_as_session_starter("Warmup") is False
+
+ def test_should_not_use_warmup_with_whitespace_as_starter(self):
+ """Test that 'Warmup' with whitespace is filtered out."""
+ assert should_use_as_session_starter(" Warmup ") is False
+ assert should_use_as_session_starter("\nWarmup\n") is False
+ assert should_use_as_session_starter("\t Warmup \t") is False
+
+ def test_should_use_warmup_in_sentence_as_starter(self):
+ """Test that messages containing 'Warmup' in a sentence are not filtered."""
+ assert (
+ should_use_as_session_starter("Let's warmup with a simple example") is True
+ )
+ assert should_use_as_session_starter("Warmup exercises are important") is True
+
+ def test_should_not_use_case_sensitive_warmup(self):
+ """Test that warmup filtering is case-sensitive (only exact 'Warmup')."""
+ # Only exact "Warmup" is filtered, not "warmup" or "WARMUP"
+ assert should_use_as_session_starter("warmup") is True
+ assert should_use_as_session_starter("WARMUP") is True
+ assert should_use_as_session_starter("WarmUp") is True
+
+
+class TestCompactIDETagsForPreview:
+ """Test compact IDE tag rendering for session previews."""
+
+ def test_compact_ide_opened_file(self):
+ """Test that
is replaced with compact indicator showing full path."""
+ text = "The user opened the file /path/to/myfile.py in the IDE.What does this do?"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đ /path/to/myfile.py" in result
+ assert "" not in result
+
+ def test_compact_ide_opened_file_without_extension(self):
+ """Test that files without extensions are handled correctly."""
+ text = "The user opened the file /Users/dain/workspace/claude-code-log/justfile in the IDE.Question"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đ /Users/dain/workspace/claude-code-log/justfile" in result
+ assert "" not in result
+
+ def test_compact_ide_selection_with_path(self):
+ """Test that shows file path when present."""
+ text = "The user selected lines 1 to 10 from /path/to/file.pyCan you explain this?"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "âī¸ /path/to/file.py" in result
+ assert "" not in result
+
+ def test_compact_ide_selection_strips_trailing_colon(self):
+ """Test that trailing colons are stripped from file paths in selections."""
+ text = "The user selected the lines 194 to 194 from /path/to/justfile:\nrelease-push\n\nThis may or may not be related.Question"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "âī¸ /path/to/justfile" in result
+ assert "âī¸ /path/to/justfile:" not in result # No trailing colon
+ assert "" not in result
+
+ def test_compact_ide_selection_without_path(self):
+ """Test that falls back to 'selection' when no path."""
+ text = "some selected code hereCan you explain this?"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "âī¸ selection" in result
+ assert "" not in result
+
+ def test_compact_ide_diagnostics(self):
+ """Test that is replaced with stethoscope emoji."""
+ text = '[{"severity": "error"}]Please fix this.'
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đŠē diagnostics" in result
+ assert "" not in result
+
+ def test_compact_multiple_ide_tags(self):
+ """Test multiple leading IDE tags are all compacted."""
+ text = (
+ "The user opened the file /src/file.py in the IDE."
+ "code"
+ "Question here"
+ )
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đ /src/file.py" in result
+ assert "âī¸" in result
+ assert "Question here" in result
+
+ def test_compact_ide_tags_no_file_path(self):
+ """Test fallback when no file path can be extracted."""
+ text = "Some content without a file pathQuestion"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đ file" in result
+ assert "" not in result
+
+ def test_compact_ide_tags_preserves_other_content(self):
+ """Test that content without IDE tags is preserved."""
+ text = "This is a normal message without any IDE tags"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert result == text
+
+ def test_embedded_ide_tags_not_replaced(self):
+ """Test that IDE tags embedded in message content (e.g., JSONL) are NOT replaced."""
+ text = (
+ "The user opened the file /path/to/file.py in the IDE."
+ 'Error: {"content":[{"text":"embedded tag"}]}'
+ )
+ result = _compact_ide_tags_for_preview(text)
+
+ # Leading tag should be compacted
+ assert "đ /path/to/file.py" in result
+ assert "" not in result
+
+ # Embedded tag should be preserved (not replaced)
+ assert "embedded tag" in result
+
+ def test_only_leading_ide_tags_processed(self):
+ """Test that only IDE tags at the start are processed, not tags later in text."""
+ text = "Some text first not at start more text"
+ result = _compact_ide_tags_for_preview(text)
+
+ # Since there's no leading IDE tag, text should be unchanged
+ assert result == text
+ assert "" in result # Tag preserved
+
+ def test_compact_bash_input(self):
+ """Test that is replaced with terminal emoji and command."""
+ text = "uv run ty check"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đģ uv run ty check" in result
+ assert "" not in result
+
+ def test_compact_bash_input_with_following_text(self):
+ """Test bash-input followed by other text."""
+ text = "git statusWhat does this show?"
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đģ git status" in result
+ assert "What does this show?" in result
+ assert "" not in result
+
+ def test_compact_bash_input_truncates_long_commands(self):
+ """Test that very long commands are truncated."""
+ long_command = "very-long-command-that-exceeds-fifty-characters-in-total-length"
+ text = f"{long_command}"
+ result = _compact_ide_tags_for_preview(text)
+
+ # Should be truncated to 47 chars + "..."
+ assert "đģ " in result
+ assert len(result.replace("đģ ", "")) <= 50
+ assert result.endswith("...")
+
+ def test_compact_bash_input_with_ide_tags(self):
+ """Test bash-input combined with IDE tags."""
+ text = (
+ "The user opened the file /src/test.py in the IDE."
+ "pytest test_file.py"
+ "Run this test"
+ )
+ result = _compact_ide_tags_for_preview(text)
+
+ assert "đ /src/test.py" in result
+ assert "đģ pytest test_file.py" in result
+ assert "Run this test" in result
+
+ def test_embedded_bash_input_not_replaced(self):
+ """Test that bash-input tags embedded in content are NOT replaced."""
+ text = 'Error message: {"content":"embedded"}'
+ result = _compact_ide_tags_for_preview(text)
+
+ # No leading tag, so text should be unchanged
+ assert result == text
+ assert "embedded" in result
+
+
+class TestCreateSessionPreview:
+ """Test session preview creation with IDE tags and truncation."""
+
+ def test_create_session_preview_uses_compact_ide_tags(self):
+ """Test that create_session_preview uses compact IDE tags with full path."""
+ text = "The user selected lines 1 to 10 from /src/utils.pyCan you refactor this function?"
+ preview = create_session_preview(text)
+
+ assert "âī¸ /src/utils.py" in preview
+ assert "Can you refactor this function?" in preview
+ assert "" not in preview
+
+ def test_create_session_preview_selection_fallback(self):
+ """Test that selection without path shows 'selection'."""
+ text = "some code hereCan you explain?"
+ preview = create_session_preview(text)
+
+ assert "âī¸ selection" in preview
+ assert "" not in preview
+
+ def test_create_session_preview_handles_truncation(self):
+ """Test that preview is truncated after IDE tag compacting."""
+ long_message = "x" * (FIRST_USER_MESSAGE_PREVIEW_LENGTH + 100)
+ preview = create_session_preview(long_message)
+
+ assert preview.endswith("...")
+ assert len(preview) == FIRST_USER_MESSAGE_PREVIEW_LENGTH + 3 # +3 for "..."
+
+ def test_create_session_preview_multiple_ide_tags(self):
+ """Test preview creation with multiple IDE tags."""
+ text = (
+ "The user opened the file /src/test.py in the IDE."
+ "Lines 1-10"
+ "Please review this code for bugs"
+ )
+ preview = create_session_preview(text)
+
+ assert "đ /src/test.py" in preview
+ assert "âī¸" in preview
+ assert "Please review this code for bugs" in preview
+
+
+class TestWarmupOnlySessionDetection:
+ """Test detection of warmup-only sessions."""
+
+ def _create_user_entry(
+ self, session_id: str, content: str, uuid: str, timestamp: str
+ ) -> UserTranscriptEntry:
+ """Helper to create a UserTranscriptEntry with all required fields."""
+ return UserTranscriptEntry(
+ type="user",
+ sessionId=session_id,
+ parentUuid=None,
+ isSidechain=False,
+ userType="external",
+ cwd="/test",
+ version="1.0.0",
+ message=UserMessage(role="user", content=content),
+ uuid=uuid,
+ timestamp=timestamp,
+ )
+
+ def _create_assistant_entry(
+ self,
+ session_id: str,
+ content: str,
+ uuid: str,
+ timestamp: str,
+ parent_uuid: str,
+ ) -> AssistantTranscriptEntry:
+ """Helper to create an AssistantTranscriptEntry with all required fields."""
+ return AssistantTranscriptEntry(
+ type="assistant",
+ sessionId=session_id,
+ parentUuid=parent_uuid,
+ isSidechain=False,
+ userType="external",
+ cwd="/test",
+ version="1.0.0",
+ message=AssistantMessage(
+ id="msg-id",
+ type="message",
+ role="assistant",
+ model="claude-3-5-sonnet",
+ content=[TextContent(type="text", text=content)],
+ ),
+ uuid=uuid,
+ timestamp=timestamp,
+ )
+
+ def test_session_with_only_warmup_messages(self):
+ """Test that a session with only warmup messages is detected."""
+ session_id = "test-session-1"
+ messages = [
+ self._create_user_entry(
+ session_id, "Warmup", "msg-1", "2025-01-01T10:00:00Z"
+ ),
+ self._create_assistant_entry(
+ session_id,
+ "I'm ready to help!",
+ "msg-2",
+ "2025-01-01T10:00:01Z",
+ "msg-1",
+ ),
+ ]
+
+ assert is_warmup_only_session(messages, session_id) is True
+
+ def test_session_with_real_messages(self):
+ """Test that a session with real messages is not detected as warmup-only."""
+ session_id = "test-session-2"
+ messages = [
+ self._create_user_entry(
+ session_id, "Hello, can you help me?", "msg-1", "2025-01-01T10:00:00Z"
+ ),
+ self._create_assistant_entry(
+ session_id, "Sure!", "msg-2", "2025-01-01T10:00:01Z", "msg-1"
+ ),
+ ]
+
+ assert is_warmup_only_session(messages, session_id) is False
+
+ def test_session_with_warmup_and_real_messages(self):
+ """Test that a session with both warmup and real messages is not warmup-only."""
+ session_id = "test-session-3"
+ messages = [
+ self._create_user_entry(
+ session_id, "Warmup", "msg-1", "2025-01-01T10:00:00Z"
+ ),
+ self._create_assistant_entry(
+ session_id, "Ready!", "msg-2", "2025-01-01T10:00:01Z", "msg-1"
+ ),
+ self._create_user_entry(
+ session_id,
+ "Now help me debug this code",
+ "msg-3",
+ "2025-01-01T10:00:02Z",
+ ),
+ ]
+
+ assert is_warmup_only_session(messages, session_id) is False
+
+ def test_session_with_multiple_warmup_messages(self):
+ """Test session with multiple warmup messages."""
+ session_id = "test-session-4"
+ messages = [
+ self._create_user_entry(
+ session_id, " Warmup ", "msg-1", "2025-01-01T10:00:00Z"
+ ),
+ self._create_user_entry(
+ session_id, "Warmup", "msg-2", "2025-01-01T10:00:01Z"
+ ),
+ ]
+
+ assert is_warmup_only_session(messages, session_id) is True
+
+ def test_nonexistent_session(self):
+ """Test checking a session ID that doesn't exist."""
+ messages = [
+ self._create_user_entry(
+ "different-session", "Hello", "msg-1", "2025-01-01T10:00:00Z"
+ ),
+ ]
+
+ # Should return False (no user messages may mean system messages exist)
+ assert is_warmup_only_session(messages, "nonexistent-session") is False
+
+ def test_empty_messages_list(self):
+ """Test with empty messages list."""
+ # Should return False (no user messages may mean system messages exist)
+ assert is_warmup_only_session([], "any-session") is False
+
+
+class TestGetWarmupSessionIds:
+ """Test bulk warmup session ID detection."""
+
+ def _create_user_entry(
+ self, session_id: str, content: str, uuid: str
+ ) -> UserTranscriptEntry:
+ """Helper to create a UserTranscriptEntry."""
+ return UserTranscriptEntry(
+ type="user",
+ sessionId=session_id,
+ parentUuid=None,
+ isSidechain=False,
+ userType="external",
+ cwd="/test",
+ version="1.0.0",
+ message=UserMessage(role="user", content=content),
+ uuid=uuid,
+ timestamp="2025-01-01T10:00:00Z",
+ )
+
+ def test_get_warmup_session_ids_multiple_sessions(self):
+ """Test get_warmup_session_ids correctly identifies warmup sessions."""
+ messages = [
+ self._create_user_entry("warmup-session", "Warmup", "msg-1"),
+ self._create_user_entry("warmup-session", "Warmup", "msg-2"),
+ self._create_user_entry("normal-session", "Hello", "msg-3"),
+ self._create_user_entry("mixed-session", "Warmup", "msg-4"),
+ self._create_user_entry("mixed-session", "Can you help?", "msg-5"),
+ ]
+ warmup_ids = get_warmup_session_ids(messages)
+
+ assert warmup_ids == {"warmup-session"}
+
+ def test_get_warmup_session_ids_no_warmup(self):
+ """Test when there are no warmup sessions."""
+ messages = [
+ self._create_user_entry("session-1", "Hello", "msg-1"),
+ self._create_user_entry("session-2", "Help me", "msg-2"),
+ ]
+ warmup_ids = get_warmup_session_ids(messages)
+
+ assert warmup_ids == set()
+
+ def test_get_warmup_session_ids_empty(self):
+ """Test with empty messages list."""
+ warmup_ids = get_warmup_session_ids([])
+
+ assert warmup_ids == set()