feat(web): inline tool activity cards with auto-collapsing (#376)

* feat(web): inline tool activity cards with auto-collapsing

Add Claude/Codex-style inline tool activity cards to the web UI that
show tool execution progress directly in the chat conversation.

While processing:
- Animated thinking dots with message text (e.g. "Calling LLM...")
- Individual tool cards with live spinner and elapsed timer
- Cards show tool name, duration, and expandable output preview

After response arrives:
- Activity group auto-collapses to "Used N tools (Xs)"
- Click summary to expand and see individual tool cards
- Click card header to see tool output in monospace

Also includes:
- "Calling LLM..." thinking status from dispatcher (all channels)
- 5-minute max timer guard to prevent leaks on dropped SSE
- Handles parallel tools, same tool twice, failures, thread switching

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(web): use frozen duration for completed tools in activity summary

The collapsed activity summary was showing inflated total duration
because finalizeActivityGroup() recalculated elapsed time from
Date.now() for already-completed tools. Now each tool card stores
its final duration at completion time and the summary uses that
frozen value instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Henry Park
2026-02-25 16:01:35 -08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 2477923af2
commit 443b120272
3 changed files with 478 additions and 8 deletions
+220
View File
@@ -483,6 +483,7 @@ body {
to { transform: rotate(360deg); }
}
.scroll-load-spinner {
display: flex;
align-items: center;
@@ -502,6 +503,225 @@ body {
animation: spin 0.6s linear infinite;
}
/* === Tool Activity Cards === */
.activity-group {
align-self: flex-start;
max-width: 80%;
padding: 4px 0 4px 12px;
border-left: 2px solid var(--border);
margin: 4px 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.activity-group.collapsed {
border-left-color: transparent;
padding-left: 0;
}
/* Thinking indicator */
.activity-thinking {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
font-size: 13px;
color: var(--text-secondary);
}
.activity-thinking-dots {
display: flex;
gap: 3px;
}
.activity-thinking-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--text-secondary);
animation: thinkingPulse 1.4s ease-in-out infinite;
}
.activity-thinking-dot:nth-child(2) { animation-delay: 0.2s; }
.activity-thinking-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes thinkingPulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
.activity-thinking-text {
font-style: italic;
}
/* Tool card */
.activity-tool-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
transition: border-color 0.2s;
}
.activity-tool-card[data-status="running"] {
border-color: rgba(52, 211, 153, 0.3);
}
.activity-tool-card[data-status="fail"] {
border-color: rgba(230, 76, 76, 0.3);
}
.activity-tool-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.activity-tool-header:hover {
background: var(--bg-tertiary);
}
.activity-tool-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
}
.activity-tool-icon .spinner {
width: 12px;
height: 12px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
.activity-icon-success {
color: var(--success);
font-size: 14px;
font-weight: 700;
line-height: 1;
}
.activity-icon-fail {
color: var(--danger);
font-size: 14px;
font-weight: 700;
line-height: 1;
}
.activity-tool-name {
font-size: 13px;
font-family: var(--font-mono);
font-weight: 500;
color: var(--text);
flex: 1;
}
.activity-tool-duration {
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-secondary);
min-width: 36px;
text-align: right;
}
.activity-tool-chevron {
font-size: 10px;
color: var(--text-secondary);
transition: transform 0.15s ease;
width: 12px;
text-align: center;
}
.activity-tool-chevron.expanded {
transform: rotate(90deg);
}
.activity-tool-body {
border-top: 1px solid var(--border);
}
.activity-tool-output {
margin: 0;
padding: 8px 10px;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.4;
color: var(--text-secondary);
background: var(--code-bg);
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
/* Collapsed summary */
.activity-summary {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
cursor: pointer;
user-select: none;
font-size: 13px;
color: var(--text-secondary);
border-radius: var(--radius);
transition: background 0.15s;
}
.activity-summary:hover {
background: var(--bg-tertiary);
}
.activity-summary-chevron {
font-size: 10px;
transition: transform 0.15s ease;
width: 12px;
text-align: center;
}
.activity-summary-chevron.expanded {
transform: rotate(90deg);
}
.activity-summary-text {
font-weight: 500;
}
.activity-summary-duration {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.7;
}
.activity-cards-container {
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 12px;
border-left: 2px solid var(--border);
margin-top: 2px;
}
@media (max-width: 768px) {
.activity-group {
max-width: 95%;
}
}
/* Approval card (inline in chat) */
.approval-card {
align-self: flex-start;