feat: interactive AskUserQuestion + WebSocket stability

- Add WebSocket heartbeat (30s ping/pong) to prevent proxy timeouts
- Add auto-reconnect with exponential backoff (1s-30s, max 10 attempts)
- Add interactive AskUserQuestion rendering with clickable options
- Add custom input field for free-text answers
- Add smooth animations (hover, selection glow, checkbox scale)
- Make interactive tool cards wider (max-w-2xl) without scrolling
- Hide error badge and result section for interactive tools
- Use TextareaAutosize for lag-free custom input
- Add Skill, SlashCommand tool renderings
- Add ThinkingBlock component for collapsible <thinking> tags

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 06:50:57 +01:00
parent 1186cb1b5e
commit eb45891d6f
4 changed files with 327 additions and 22 deletions

View File

@@ -9,6 +9,11 @@ function getWsUrl() {
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
// Reconnect configuration
const RECONNECT_DELAY_BASE = 1000; // 1 second
const RECONNECT_DELAY_MAX = 30000; // 30 seconds
const RECONNECT_MAX_ATTEMPTS = 10;
export function useClaudeSession() {
const [connected, setConnected] = useState(false);
const [sessionActive, setSessionActive] = useState(false);
@@ -50,6 +55,9 @@ export function useClaudeSession() {
const wsRef = useRef(null);
const currentAssistantMessage = useRef(null);
const reconnectAttempts = useRef(0);
const reconnectTimeout = useRef(null);
const intentionalClose = useRef(false);
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) return;
@@ -61,13 +69,34 @@ export function useClaudeSession() {
console.log('WebSocket connected');
setConnected(true);
setError(null);
reconnectAttempts.current = 0; // Reset on successful connection
};
ws.onclose = () => {
console.log('WebSocket disconnected');
ws.onclose = (event) => {
console.log('WebSocket disconnected', { code: event.code, reason: event.reason, wasClean: event.wasClean });
setConnected(false);
setSessionActive(false);
setIsProcessing(false);
// Attempt reconnect unless it was intentional or auth failure
if (!intentionalClose.current && event.code !== 1008) {
if (reconnectAttempts.current < RECONNECT_MAX_ATTEMPTS) {
const delay = Math.min(
RECONNECT_DELAY_BASE * Math.pow(2, reconnectAttempts.current),
RECONNECT_DELAY_MAX
);
reconnectAttempts.current++;
console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts.current}/${RECONNECT_MAX_ATTEMPTS})`);
reconnectTimeout.current = setTimeout(() => {
console.log('Attempting reconnect...');
connect();
}, delay);
} else {
console.log('Max reconnect attempts reached');
setError('Connection lost. Please refresh the page.');
}
}
};
ws.onerror = (err) => {
@@ -543,8 +572,13 @@ export function useClaudeSession() {
// Auto-connect on mount
useEffect(() => {
intentionalClose.current = false;
connect();
return () => {
intentionalClose.current = true;
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
}
wsRef.current?.close();
};
}, [connect]);