feat: Introduce Nucleo as the main AI agent identity

- Add Nucleo atom logo with animated orbiting electrons
- Redesign FAB with glassmorphism effect and purple gradient
- Add connection indicator (green dot) when terminal is open
- Update FloatingTerminal header with Nucleo branding
- Add PermissionRequest hook support with red alert animation
- Document Nucleo in README with visual states table
- Create official Nucleo logo SVG in docs/
This commit is contained in:
2026-02-14 02:56:50 -06:00
parent d9e2548fb8
commit 47f5524416
6 changed files with 368 additions and 52 deletions

View File

@@ -2,6 +2,50 @@
Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol). Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).
---
## Nucleo
<p align="center">
<img src="docs/nucleo-logo.svg" alt="Nucleo" width="120"/>
</p>
**Nucleo** is the main AI agent powering Agent UI. It serves as the bridge between Claude Code and your visual interface, providing real-time status feedback through an animated FAB (Floating Action Button).
### Visual States
Nucleo communicates its current state through distinct animations:
| State | Color | Animation | Trigger |
|-------|-------|-----------|---------|
| **Idle** | Purple | Rotating atom | Default state |
| **Processing** | Orange | Pulsing dots | User prompt submitted |
| **Reading** | Cyan | Eye icon + scan | Reading files (Read/Glob/Grep) |
| **Writing** | Green | Pencil icon + pulse | Writing files (Edit/Write) |
| **Subagent** | Purple | Orbital ring | Task tool spawned |
| **Permission** | Red | Alert + shake | Awaiting user permission |
| **Session Start** | Green | Ripple waves | Session initialized |
| **Notification** | Yellow | Bounce | System notification |
### Integration
Nucleo's status is synchronized via Claude Code hooks:
```json
{
"hooks": {
"UserPromptSubmit": [{ "hooks": [{ "command": "... status: processing ..." }] }],
"PreToolUse": [{ "hooks": [{ "command": "... status: toolUse ..." }] }],
"PostToolUse": [{ "hooks": [{ "command": "... status: toolDone ..." }] }],
"Stop": [{ "hooks": [{ "command": "... status: idle ..." }] }]
}
}
```
The FAB receives these status updates via WebSocket and displays the corresponding animation, giving you real-time visibility into what Claude is doing.
---
## Overview ## Overview
Agent UI provides a visual canvas where Claude Code can render dynamic Vue 3 components, HTML content, and interactive UIs in real-time. It bridges the gap between CLI-based AI assistance and rich visual interfaces. Agent UI provides a visual canvas where Claude Code can render dynamic Vue 3 components, HTML content, and interactive UIs in real-time. It bridges the gap between CLI-based AI assistance and rich visual interfaces.

61
docs/nucleo-logo.svg Normal file
View File

@@ -0,0 +1,61 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="coreGradient" cx="50%" cy="30%" r="60%">
<stop offset="0%" stop-color="#c4b5fd"/>
<stop offset="50%" stop-color="#a78bfa"/>
<stop offset="100%" stop-color="#6366f1"/>
</radialGradient>
<linearGradient id="orbitGradient1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#818cf8" stop-opacity="0.9"/>
<stop offset="50%" stop-color="#a78bfa" stop-opacity="0.5"/>
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.9"/>
</linearGradient>
<linearGradient id="electronGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e0e7ff"/>
<stop offset="100%" stop-color="#a78bfa"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="coreGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background circle (subtle) -->
<circle cx="60" cy="60" r="55" fill="none" stroke="#4f46e5" stroke-width="1" opacity="0.2"/>
<!-- Orbital rings -->
<ellipse cx="60" cy="60" rx="45" ry="18" stroke="url(#orbitGradient1)" stroke-width="2" fill="none" transform="rotate(-30 60 60)" opacity="0.7"/>
<ellipse cx="60" cy="60" rx="45" ry="18" stroke="url(#orbitGradient1)" stroke-width="2" fill="none" transform="rotate(30 60 60)" opacity="0.5"/>
<ellipse cx="60" cy="60" rx="45" ry="18" stroke="url(#orbitGradient1)" stroke-width="2" fill="none" transform="rotate(90 60 60)" opacity="0.6"/>
<!-- Core nucleus with glow -->
<circle cx="60" cy="60" r="18" fill="url(#coreGradient)" filter="url(#coreGlow)"/>
<circle cx="60" cy="60" r="14" fill="url(#coreGradient)" opacity="0.8"/>
<circle cx="55" cy="55" r="5" fill="white" opacity="0.4"/>
<!-- Electrons with glow -->
<circle cx="60" cy="15" r="7" fill="url(#electronGradient)" filter="url(#glow)">
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="360 60 60" dur="4s" repeatCount="indefinite"/>
</circle>
<circle cx="100" cy="70" r="6" fill="url(#electronGradient)" filter="url(#glow)">
<animateTransform attributeName="transform" type="rotate" from="120 60 60" to="480 60 60" dur="5s" repeatCount="indefinite"/>
</circle>
<circle cx="20" cy="70" r="6" fill="url(#electronGradient)" filter="url(#glow)">
<animateTransform attributeName="transform" type="rotate" from="240 60 60" to="600 60 60" dur="4.5s" repeatCount="indefinite"/>
</circle>
<!-- Small accent electrons -->
<circle cx="60" cy="105" r="4" fill="#c4b5fd" opacity="0.6">
<animateTransform attributeName="transform" type="rotate" from="180 60 60" to="540 60 60" dur="6s" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -25,7 +25,7 @@ const responseRef = ref<InstanceType<typeof FloatingResponse> | null>(null)
const canvasStore = useCanvasStore() const canvasStore = useCanvasStore()
// Claude status state (for FAB animations) // Claude status state (for FAB animations)
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'thinking' type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'permissionRequest' | 'thinking'
const claudeStatus = ref<ClaudeStatus>('idle') const claudeStatus = ref<ClaudeStatus>('idle')
const claudeTool = ref<string | null>(null) const claudeTool = ref<string | null>(null)
@@ -33,6 +33,7 @@ const isProcessing = ref(false) // Main processing state (UserPromptSubmit
const isReading = ref(false) // Reading files const isReading = ref(false) // Reading files
const isWriting = ref(false) // Writing files const isWriting = ref(false) // Writing files
const hasSubagent = ref(false) // Subagent active const hasSubagent = ref(false) // Subagent active
const awaitingPermission = ref(false) // Waiting for user permission (highest priority)
const showSessionStart = ref(false) // Session start animation (3s) const showSessionStart = ref(false) // Session start animation (3s)
const showNotification = ref(false) // Notification pulse (2s) const showNotification = ref(false) // Notification pulse (2s)
const showToolFlash = ref(false) // Tool use flash (500ms) const showToolFlash = ref(false) // Tool use flash (500ms)
@@ -78,9 +79,14 @@ function connectStatusWs() {
isProcessing.value = false isProcessing.value = false
isReading.value = false isReading.value = false
isWriting.value = false isWriting.value = false
awaitingPermission.value = false
if (processingTimeout) clearTimeout(processingTimeout) if (processingTimeout) clearTimeout(processingTimeout)
break break
case 'permissionRequest':
awaitingPermission.value = true
break
case 'reading': case 'reading':
isReading.value = true isReading.value = true
triggerToolFlash() triggerToolFlash()
@@ -98,6 +104,7 @@ function connectStatusWs() {
case 'toolDone': case 'toolDone':
isReading.value = false isReading.value = false
isWriting.value = false isWriting.value = false
awaitingPermission.value = false
break break
case 'sessionStart': case 'sessionStart':
@@ -278,12 +285,13 @@ watch(() => route.name, (newPage) => {
reading: isReading, reading: isReading,
writing: isWriting, writing: isWriting,
subagent: hasSubagent, subagent: hasSubagent,
permission: awaitingPermission,
'session-start': showSessionStart, 'session-start': showSessionStart,
notification: showNotification, notification: showNotification,
'tool-flash': showToolFlash 'tool-flash': showToolFlash
}" }"
@click="showTerminal = !showTerminal" @click="showTerminal = !showTerminal"
:title="isProcessing ? `Claude: ${claudeTool || 'processing'}` : 'Toggle Terminal'" :title="awaitingPermission ? `Permiso requerido: ${claudeTool || 'herramienta'}` : isProcessing ? `Claude: ${claudeTool || 'processing'}` : 'Toggle Terminal'"
> >
<!-- Subagent orbital ring --> <!-- Subagent orbital ring -->
<svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100"> <svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100">
@@ -297,8 +305,15 @@ watch(() => route.name, (newPage) => {
<span></span> <span></span>
</div> </div>
<!-- Permission request icon (alert/hand) - highest priority -->
<svg v-if="awaitingPermission" class="permission-icon" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<!-- Processing animation (three dots) --> <!-- Processing animation (three dots) -->
<div v-if="isProcessing && !isReading && !isWriting" class="thinking-dots"> <div v-else-if="isProcessing && !isReading && !isWriting" class="thinking-dots">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
@@ -320,15 +335,27 @@ watch(() => route.name, (newPage) => {
<!-- Normal icons --> <!-- Normal icons -->
<template v-else> <template v-else>
<svg v-if="!showTerminal" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <!-- Terminal closed: Nucleo atom icon -->
<polyline points="4 17 10 11 4 5"></polyline> <svg v-if="!showTerminal" class="nucleo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none">
<line x1="12" y1="19" x2="20" y2="19"></line> <!-- Core nucleus -->
<circle cx="12" cy="12" r="3.5" fill="white" opacity="0.95"/>
<!-- Orbital rings -->
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.7" transform="rotate(-30 12 12)"/>
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.5" transform="rotate(30 12 12)"/>
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.6" transform="rotate(90 12 12)"/>
<!-- Electrons -->
<circle cx="12" cy="4" r="1.8" fill="white" opacity="0.9"/>
<circle cx="19" cy="14" r="1.8" fill="white" opacity="0.7"/>
<circle cx="5" cy="14" r="1.8" fill="white" opacity="0.8"/>
</svg> </svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <!-- Terminal open: Minimize chevron with connection indicator -->
<line x1="18" y1="6" x2="6" y2="18"></line> <template v-else>
<line x1="6" y1="6" x2="18" y2="18"></line> <span class="connection-dot"></span>
<svg class="minimize-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</template> </template>
</template>
</button> </button>
<!-- Voice FAB Button --> <!-- Voice FAB Button -->
@@ -419,35 +446,102 @@ watch(() => route.name, (newPage) => {
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
width: 56px; width: 58px;
height: 56px; height: 58px;
border-radius: 16px; border-radius: 18px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); background: linear-gradient(145deg, #7c3aed 0%, #6366f1 50%, #8b5cf6 100%);
color: white; color: white;
border: none; border: 2px solid rgba(255, 255, 255, 0.15);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4); box-shadow:
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 0 4px 15px rgba(124, 58, 237, 0.4),
0 8px 30px rgba(99, 102, 241, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 9998; z-index: 9998;
overflow: visible; overflow: visible;
backdrop-filter: blur(10px);
}
.terminal-fab::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 21px;
background: linear-gradient(145deg, rgba(139, 92, 246, 0.5), rgba(99, 102, 241, 0.2));
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
} }
.terminal-fab:hover { .terminal-fab:hover {
transform: scale(1.05); transform: translateY(-3px) scale(1.05);
box-shadow: 0 12px 32px rgba(99, 102, 241, 0.5); box-shadow:
0 8px 25px rgba(124, 58, 237, 0.5),
0 15px 40px rgba(99, 102, 241, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
} }
.terminal-fab:hover::before {
opacity: 1;
}
/* Nucleo atom icon animation */
.nucleo-icon {
animation: nucleo-orbit 8s linear infinite;
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
.nucleo-icon ellipse {
transform-origin: center;
}
/* Terminal active - Connected state */
.terminal-fab.active { .terminal-fab.active {
background: #ef4444; background: linear-gradient(145deg, #6366f1 0%, #4f46e5 50%, #7c3aed 100%);
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4); border-color: rgba(16, 185, 129, 0.5);
transform: rotate(90deg); box-shadow:
0 4px 15px rgba(99, 102, 241, 0.4),
0 8px 30px rgba(79, 70, 229, 0.3),
0 0 20px rgba(16, 185, 129, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transform: none;
} }
.terminal-fab.active:hover { .terminal-fab.active:hover {
box-shadow: 0 12px 32px rgba(239, 68, 68, 0.5); transform: translateY(-2px);
box-shadow:
0 6px 20px rgba(99, 102, 241, 0.5),
0 12px 35px rgba(79, 70, 229, 0.35),
0 0 25px rgba(16, 185, 129, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
/* Connection indicator dot */
.connection-dot {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
background: #10b981;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
animation: connection-pulse 2s ease-in-out infinite;
}
/* Minimize icon */
.minimize-icon {
opacity: 0.9;
transition: transform 0.2s ease;
}
.terminal-fab.active:hover .minimize-icon {
transform: translateY(2px);
} }
/* Processing state (UserPromptSubmit → Stop) - Orange pulsing */ /* Processing state (UserPromptSubmit → Stop) - Orange pulsing */
@@ -492,6 +586,25 @@ watch(() => route.name, (newPage) => {
animation: notification-bounce 0.5s ease-in-out 4 !important; animation: notification-bounce 0.5s ease-in-out 4 !important;
} }
/* Permission Request - HIGHEST PRIORITY - Red pulsing alert */
.terminal-fab.permission {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7) !important;
animation: permission-pulse 1s ease-in-out infinite !important;
transform: rotate(0deg) scale(1.1) !important;
z-index: 10000 !important;
}
.terminal-fab.permission:hover {
transform: scale(1.15) !important;
}
/* Permission icon animation */
.permission-icon {
animation: permission-shake 0.5s ease-in-out infinite;
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
}
/* Tool flash - Quick white flash */ /* Tool flash - Quick white flash */
.terminal-fab.tool-flash::after { .terminal-fab.tool-flash::after {
content: ''; content: '';
@@ -643,6 +756,48 @@ watch(() => route.name, (newPage) => {
} }
} }
@keyframes permission-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7), 0 8px 24px rgba(239, 68, 68, 0.5);
transform: scale(1.1);
}
50% {
box-shadow: 0 0 0 15px rgba(239, 68, 68, 0), 0 8px 40px rgba(239, 68, 68, 0.8);
transform: scale(1.15);
}
}
@keyframes permission-shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-5deg); }
75% { transform: rotate(5deg); }
}
@keyframes nucleo-orbit {
0% {
transform: rotate(0deg);
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
50% {
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.9));
}
100% {
transform: rotate(360deg);
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
}
@keyframes connection-pulse {
0%, 100% {
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
transform: scale(1);
}
50% {
box-shadow: 0 0 12px rgba(16, 185, 129, 1), 0 0 20px rgba(16, 185, 129, 0.4);
transform: scale(1.1);
}
}
/* Voice FAB */ /* Voice FAB */
.voice-fab { .voice-fab {
position: fixed; position: fixed;
@@ -681,13 +836,20 @@ watch(() => route.name, (newPage) => {
.terminal-fab { .terminal-fab {
bottom: 16px; bottom: 16px;
right: 16px; right: 16px;
width: 52px; width: 54px;
height: 52px; height: 54px;
border-radius: 16px;
} }
.terminal-fab.active:not(.processing):not(.reading):not(.writing) { .terminal-fab::before {
opacity: 0; border-radius: 19px;
pointer-events: none; }
.connection-dot {
width: 8px;
height: 8px;
top: 6px;
right: 6px;
} }
.orbital-ring { .orbital-ring {

View File

@@ -498,10 +498,40 @@ defineExpose({
<!-- Titlebar --> <!-- Titlebar -->
<div class="titlebar" @mousedown="startDrag"> <div class="titlebar" @mousedown="startDrag">
<div class="left"> <div class="left">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <!-- Nucleo Logo -->
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/> <div class="nucleo-logo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<!-- Core nucleus -->
<circle cx="12" cy="12" r="4" fill="url(#nucleoGradient)"/>
<!-- Orbital rings -->
<ellipse cx="12" cy="12" rx="9" ry="4" stroke="url(#orbitGradient)" stroke-width="1.2" fill="none" transform="rotate(-30 12 12)"/>
<ellipse cx="12" cy="12" rx="9" ry="4" stroke="url(#orbitGradient)" stroke-width="1.2" fill="none" transform="rotate(30 12 12)"/>
<ellipse cx="12" cy="12" rx="9" ry="4" stroke="url(#orbitGradient)" stroke-width="1.2" fill="none" transform="rotate(90 12 12)"/>
<!-- Electrons -->
<circle cx="12" cy="3" r="1.5" fill="#a78bfa">
<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="3s" repeatCount="indefinite"/>
</circle>
<circle cx="20" cy="14" r="1.5" fill="#818cf8">
<animateTransform attributeName="transform" type="rotate" from="120 12 12" to="480 12 12" dur="4s" repeatCount="indefinite"/>
</circle>
<circle cx="4" cy="14" r="1.5" fill="#c4b5fd">
<animateTransform attributeName="transform" type="rotate" from="240 12 12" to="600 12 12" dur="3.5s" repeatCount="indefinite"/>
</circle>
<!-- Gradients -->
<defs>
<radialGradient id="nucleoGradient" cx="50%" cy="30%" r="60%">
<stop offset="0%" stop-color="#a78bfa"/>
<stop offset="100%" stop-color="#6366f1"/>
</radialGradient>
<linearGradient id="orbitGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#818cf8" stop-opacity="0.8"/>
<stop offset="50%" stop-color="#a78bfa" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.8"/>
</linearGradient>
</defs>
</svg> </svg>
<span>Terminal</span> </div>
<span class="nucleo-name">Nucleo</span>
<i class="dot" :class="{ on: connected, wait: connecting }"></i> <i class="dot" :class="{ on: connected, wait: connecting }"></i>
<a v-if="!connected && !connecting" class="link" @click.stop="connect">connect</a> <a v-if="!connected && !connecting" class="link" @click.stop="connect">connect</a>
</div> </div>
@@ -565,11 +595,28 @@ defineExpose({
.left { .left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 6px;
color: #222; color: #222;
font: 500 10px/1 system-ui, sans-serif; font: 500 10px/1 system-ui, sans-serif;
} }
.nucleo-logo {
display: flex;
align-items: center;
justify-content: center;
filter: drop-shadow(0 1px 2px rgba(99, 102, 241, 0.3));
}
.nucleo-name {
font-weight: 600;
font-size: 11px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.5px;
}
.dot { .dot {
width: 5px; height: 5px; width: 5px; height: 5px;
border-radius: 50%; border-radius: 50%;

View File

@@ -12,6 +12,7 @@ type ClaudeStatus =
| 'subagentStart' // SubagentStart - Spawning subagent | 'subagentStart' // SubagentStart - Spawning subagent
| 'subagentStop' // SubagentStop - Subagent finished | 'subagentStop' // SubagentStop - Subagent finished
| 'notification' // Notification - Claude sent notification | 'notification' // Notification - Claude sent notification
| 'permissionRequest' // PermissionRequest - Waiting for user permission
| 'thinking' // Legacy support | 'thinking' // Legacy support
interface ClaudeStatusPayload { interface ClaudeStatusPayload {
@@ -27,7 +28,8 @@ export async function handleClaudeStatus(req: Request): Promise<Response | null>
const validStatuses: ClaudeStatus[] = [ const validStatuses: ClaudeStatus[] = [
'idle', 'processing', 'toolUse', 'toolDone', 'reading', 'writing', 'idle', 'processing', 'toolUse', 'toolDone', 'reading', 'writing',
'sessionStart', 'subagentStart', 'subagentStop', 'notification', 'thinking' 'sessionStart', 'subagentStart', 'subagentStop', 'notification',
'permissionRequest', 'thinking'
] ]
if (!body.status || !validStatuses.includes(body.status)) { if (!body.status || !validStatuses.includes(body.status)) {
return errorResponse(`Invalid status. Must be one of: ${validStatuses.join(', ')}`, 400) return errorResponse(`Invalid status. Must be one of: ${validStatuses.join(', ')}`, 400)

View File

@@ -214,7 +214,7 @@ export function startTerminalServer() {
} }
// Claude status types // Claude status types
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'thinking' type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'permissionRequest' | 'thinking'
// Broadcast Claude status to ALL clients across ALL sessions // Broadcast Claude status to ALL clients across ALL sessions
export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string) { export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string) {