feat: Add rich Claude status animations to FAB
- Add multiple hook-driven animations for FAB (processing, reading, writing, subagent, sessionStart, notification) - Create claude-status.ts route to handle status updates from Claude Code hooks - Broadcast status via WebSocket to all connected clients - Processing (UserPromptSubmit→Stop): orange pulsing dots - Reading (Read/Glob/Grep): cyan eye icon with scan animation - Writing (Edit/Write): green pencil icon with pulse - Subagent: purple orbital ring animation - SessionStart: green wake-up ripple effect (3s) - Notification: yellow bounce animation (2s) - Tool flash: quick white flash on any tool use
This commit is contained in:
51
server/routes/claude-status.ts
Normal file
51
server/routes/claude-status.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { jsonResponse, errorResponse } from '../utils/cors'
|
||||
import { PORT_TERMINAL } from '../config'
|
||||
|
||||
type ClaudeStatus =
|
||||
| 'idle'
|
||||
| 'processing' // UserPromptSubmit - Claude is processing user input
|
||||
| 'toolUse' // PreToolUse - Using a tool (generic)
|
||||
| 'toolDone' // PostToolUse - Tool finished
|
||||
| 'reading' // PreToolUse(Read/Glob/Grep) - Reading files
|
||||
| 'writing' // PreToolUse(Edit/Write) - Writing files
|
||||
| 'sessionStart' // SessionStart - Session just started
|
||||
| 'subagentStart' // SubagentStart - Spawning subagent
|
||||
| 'subagentStop' // SubagentStop - Subagent finished
|
||||
| 'notification' // Notification - Claude sent notification
|
||||
| 'thinking' // Legacy support
|
||||
|
||||
interface ClaudeStatusPayload {
|
||||
status: ClaudeStatus
|
||||
tool?: string
|
||||
}
|
||||
|
||||
export async function handleClaudeStatus(req: Request): Promise<Response | null> {
|
||||
if (req.method !== 'POST') return null
|
||||
|
||||
try {
|
||||
const body = await req.json() as ClaudeStatusPayload
|
||||
|
||||
const validStatuses: ClaudeStatus[] = [
|
||||
'idle', 'processing', 'toolUse', 'toolDone', 'reading', 'writing',
|
||||
'sessionStart', 'subagentStart', 'subagentStop', 'notification', 'thinking'
|
||||
]
|
||||
if (!body.status || !validStatuses.includes(body.status)) {
|
||||
return errorResponse(`Invalid status. Must be one of: ${validStatuses.join(', ')}`, 400)
|
||||
}
|
||||
|
||||
// Forward to terminal server for WebSocket broadcast
|
||||
try {
|
||||
await fetch(`http://localhost:${PORT_TERMINAL}/claude-status`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[claude-status] Failed to forward to terminal server:', e)
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true, status: body.status })
|
||||
} catch (e) {
|
||||
return errorResponse('Invalid JSON body', 400)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { handleGiteaRepo, handleGiteaTree, handleGiteaFile } from './gitea'
|
||||
import { handleTables, handleStats, handleTableSchema, handleTableData, handleQuery } from './database'
|
||||
import { handleWhisperRoutes } from './whisper'
|
||||
import { handleRecordingsRoutes } from './recordings'
|
||||
import { handleClaudeStatus } from './claude-status'
|
||||
|
||||
export async function handleRequest(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url)
|
||||
@@ -42,6 +43,12 @@ export async function handleRequest(req: Request): Promise<Response> {
|
||||
if (res) return res
|
||||
}
|
||||
|
||||
// Claude Code status (thinking/idle)
|
||||
if (path === '/api/claude-status') {
|
||||
const res = await handleClaudeStatus(req)
|
||||
if (res) return res
|
||||
}
|
||||
|
||||
// Components
|
||||
if (path === '/api/components') {
|
||||
const res = await handleComponents(req)
|
||||
|
||||
@@ -77,7 +77,7 @@ function getOrCreateSession(sessionId: string = DEFAULT_SESSION_ID): TerminalSes
|
||||
export function startTerminalServer() {
|
||||
const server = Bun.serve({
|
||||
port: PORT_TERMINAL,
|
||||
fetch(req, server) {
|
||||
async fetch(req, server) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
const corsHeaders = {
|
||||
@@ -113,6 +113,17 @@ export function startTerminalServer() {
|
||||
return Response.json({ sessions: list })
|
||||
}
|
||||
|
||||
// Claude status broadcast endpoint
|
||||
if (url.pathname === '/claude-status' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await req.json() as { status: ClaudeStatus, tool?: string }
|
||||
broadcastClaudeStatus(body.status, body.tool)
|
||||
return Response.json({ success: true }, { headers: corsHeaders })
|
||||
} catch {
|
||||
return Response.json({ error: 'Invalid JSON' }, { status: 400, headers: corsHeaders })
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a WebSocket upgrade request
|
||||
const upgradeHeader = req.headers.get('upgrade')
|
||||
console.log(`[Terminal] Request: ${req.method} ${url.pathname}, Upgrade: ${upgradeHeader}`)
|
||||
@@ -201,3 +212,30 @@ export function startTerminalServer() {
|
||||
console.log(`[Terminal] WebSocket running at ws://localhost:${PORT_TERMINAL}`)
|
||||
return server
|
||||
}
|
||||
|
||||
// Claude status types
|
||||
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'thinking'
|
||||
|
||||
// Broadcast Claude status to ALL clients across ALL sessions
|
||||
export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string) {
|
||||
const message = JSON.stringify({
|
||||
type: 'claude-status',
|
||||
status,
|
||||
tool,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
let clientCount = 0
|
||||
for (const [, session] of sessions) {
|
||||
for (const ws of session.clients) {
|
||||
try {
|
||||
ws.send(message)
|
||||
clientCount++
|
||||
} catch {
|
||||
// Client disconnected, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Terminal] Claude status broadcast: ${status}${tool ? ` (${tool})` : ''} → ${clientCount} clients`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user