xterm-js
This skill should be used when working with xterm.js terminal implementations, React-based terminal applications, WebSocket terminal communication, or refactoring terminal-related code. It provides battle-tested patterns, common pitfalls, and debugging strategies learned from building production terminal applications.
What this skill does
# xterm.js Best Practices
## Overview
This skill provides comprehensive best practices for building terminal applications with xterm.js, React, and WebSockets. It captures critical patterns discovered through debugging production terminal applications, including state management, WebSocket communication, React hooks integration, and terminal lifecycle management.
## When to Use This Skill
Use this skill when:
- Building or debugging xterm.js terminal implementations
- Integrating xterm.js with React (hooks, state, refs)
- Implementing WebSocket-based terminal I/O
- Managing terminal persistence with tmux or similar backends
- Refactoring terminal-related React components into custom hooks
- Debugging terminal initialization, resize, or rendering issues
- Implementing split terminal layouts or multi-window terminal management
- Working on detach/reattach terminal functionality
## Core Best Practices
### 1. Refs and State Management
**Critical Pattern: Clear Refs When State Changes**
Refs persist across state changes. When clearing state, also clear related refs.
```typescript
// CORRECT - Clear both state AND ref
if (terminal.agentId) {
clearProcessedAgentId(terminal.agentId) // Clear ref
}
updateTerminal(id, { agentId: undefined }) // Clear state
```
**Key Insight:**
- State (Zustand/Redux) = what the terminal is
- Refs (useRef) = what we've processed
- When state changes, check if related refs need updating
**Common Scenario:** Detach/reattach workflows where the same agentId returns from backend. Without clearing the ref, the frontend thinks it already processed this agentId and ignores reconnection messages.
See `references/refs-state-patterns.md` for detailed examples.
### 2. WebSocket Message Types
**Critical Pattern: Know Your Destructive Operations**
Backend WebSocket handlers often have different semantics for similar-looking message types:
- `type: 'disconnect'` - Graceful disconnect, keep session alive
- `type: 'close'` - **FORCE CLOSE and KILL session** (destructive!)
```typescript
// WRONG - This KILLS the tmux session!
wsRef.current.send(JSON.stringify({
type: 'close',
terminalId: terminal.agentId,
}))
// CORRECT - For detach, use API endpoint only
await fetch(`/api/tmux/detach/${sessionName}`, { method: 'POST' })
// Don't send WebSocket message - let PTY disconnect naturally
```
**Key Insight:** Read backend code to understand what each message type does. "Close" often means "destroy" in WebSocket contexts.
See `references/websocket-patterns.md` for backend routing patterns.
### 3. React Hooks & Refactoring
**Critical Pattern: Identify Shared Refs Before Extracting Hooks**
When extracting custom hooks that manage shared resources:
```typescript
// WRONG - Hook creates its own ref
export function useWebSocketManager(...) {
const wsRef = useRef<WebSocket | null>(null) // Creates NEW ref!
}
// RIGHT - Hook uses shared ref from parent
export function useWebSocketManager(
wsRef: React.MutableRefObject<WebSocket | null>, // Pass as parameter
...
) {
// Uses parent's ref - all components share same WebSocket
}
```
**Checklist Before Extracting Hooks:**
- [ ] Map out all refs (diagram which components use which refs)
- [ ] Check if ref is used outside the hook
- [ ] If ref is shared → pass as parameter, don't create internally
- [ ] Test with real usage immediately after extraction
See `references/react-hooks-patterns.md` for refactoring workflows.
### 4. Terminal Initialization
**Critical Pattern: xterm.js Requires Non-Zero Container Dimensions**
xterm.js cannot initialize on containers with 0x0 dimensions. Use visibility-based hiding, not display:none.
```typescript
// WRONG - Prevents xterm initialization
<div style={{ display: isActive ? 'block' : 'none' }}>
<Terminal />
</div>
// CORRECT - All terminals get dimensions, use visibility to hide
<div style={{
position: 'absolute',
top: 0, left: 0, right: 0, bottom: 0,
visibility: isActive ? 'visible' : 'hidden',
zIndex: isActive ? 1 : 0,
}}>
<Terminal />
</div>
```
**Why This Works:**
- All terminals render with full dimensions (stacked via absolute positioning)
- xterm.js can initialize properly on all terminals
- `visibility: hidden` hides inactive terminals without removing dimensions
- Use `isSelected` prop to trigger refresh when tab becomes active
**Common Scenario:** Tab-based terminal UI where switching tabs should show different terminals. After refresh, only active tab would render if using `display: none`.
### 5. useEffect Dependencies for Initialization
**Critical Pattern: Early Returns Need Corresponding Dependencies**
If a useEffect checks a ref and returns early, include `ref.current` in dependencies so it re-runs when ref becomes available.
```typescript
// WRONG - Only runs once, may return early forever
useEffect(() => {
if (!terminalRef.current) return // Returns if null
// Setup ResizeObserver
}, []) // Never re-runs!
// CORRECT - Re-runs when ref becomes available
useEffect(() => {
if (!terminalRef.current) return
// Setup ResizeObserver
}, [terminalRef.current]) // Re-runs when ref changes!
```
**Common Pattern:** Wait for DOM refs AND library instances (xterm, fitAddon) before setup:
```typescript
useEffect(() => {
if (!terminalRef.current?.parentElement ||
!xtermRef.current ||
!fitAddonRef.current) {
return // Wait for all refs
}
// Setup ResizeObserver
}, [terminalRef.current, xtermRef.current, fitAddonRef.current])
```
### 6. Session Naming & Reconnection
**Critical Pattern: Use Consistent Session Identifiers**
When reconnecting, use the existing `sessionName` to find the existing PTY. Don't generate a new one.
```typescript
// CORRECT - Reconnect to existing session
const config = {
sessionName: terminal.sessionName, // Use existing!
resumable: true,
useTmux: true,
}
// WRONG - Would create new session
const config = {
sessionName: generateNewSessionName(), // DON'T DO THIS
}
```
**Key Insight:** Tmux sessions have stable names. Use them as the source of truth for reconnection.
### 7. Multi-Window Terminal Management
**Critical Pattern: Backend Output Routing Must Use Ownership Tracking**
For multi-window setups, track which WebSocket connections own which terminals. Never broadcast terminal output to all clients.
```javascript
// Backend: Track ownership
const terminalOwners = new Map() // terminalId -> Set<WebSocket>
// On output: send ONLY to owners (no broadcast!)
terminalRegistry.on('output', (terminalId, data) => {
const owners = terminalOwners.get(terminalId)
owners.forEach(client => client.send(message))
})
```
**Why:** Broadcasting terminal output causes escape sequence corruption (DSR sequences) in wrong windows.
**Frontend Pattern:** Filter terminals by windowId before adding to agents:
```typescript
// Check windowId BEFORE adding to webSocketAgents
if (existingTerminal) {
const terminalWindow = existingTerminal.windowId || 'main'
if (terminalWindow !== currentWindowId) {
return // Ignore terminals from other windows
}
// Now safe to add to webSocketAgents
}
```
See CLAUDE.md "Multi-Window Support - Critical Architecture" section for complete flow.
### 8. Testing Workflows
**Critical Pattern: Test Real Usage Immediately After Refactoring**
TypeScript compilation ≠ working code. Always test with real usage:
```bash
# After refactoring:
npm run build # 1. Check TypeScript
# Open http://localhost:5173
# Spawn terminal # 2. Test spawning
# Type in terminal # 3. Test input (WebSocket)
# Resize window # 4. Test resize (ResizeObserver)
# Spawn TUI tool # 5. Test complex interactions
```
**Refactoring Checklist:**
- [ ] TypeScript compilation succeeds
- [ ] Spawn a terminal (test spawning logic)
- [ ] Type in terminal (test WebSocket communication)
- [ ] Resize window (test ResizeObserver)
- [ ] Spawn TUI tool like htop (test complex ANSI sequences)
- [ ] Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.