InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

18-mailbox-notifications

Shared from "Claude-Code" on Inkdown

Cross-Component Communication: Mailbox & Notifications

Overview

In a complex terminal app, components need to talk to each other without being directly connected. The Mailbox and Notification systems provide decoupled, asynchronous communication across the React tree.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                    COMMUNICATION PATTERNS                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                     DIRECT (Props) - AVOID                            │   │
│  │                                                                      │   │
│  │   Parent ──► Child ──► Grandchild                                    │   │
│  │      │         │           │                                         │   │
│  │      ▼         ▼           ▼                                         │   │
│  │   Props    Props      Props         ❌ Tight coupling               │   │
│  │   drilling drilling   drilling      ❌ Hard to refactor             │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                     MAILBOX PATTERN (PREFERRED)                     │   │
│  │                                                                      │   │
│  │   ┌─────────┐        ┌─────────────┐        ┌─────────┐          │   │
│  │   │ Tool A  │───────►│             │◄───────│ Tool B  │          │   │
│  │   │ (sends) │        │   MAILBOX   │        │ (sends) │          │   │
│  │   └─────────┘        │  (central)  │        └─────────┘          │   │
│  │                      │             │                               │   │
│  │   ┌─────────┐        │             │        ┌─────────┐          │   │
│  │   │  UI C   │◄───────│             │───────►│  UI D   │          │   │
│  │   │(receives)│       └─────────────┘        │(receives)│          │   │
│  │   └─────────┘                                └─────────┘          │   │
│  │                                                                      │   │
│  │   ✅ Decoupled      ✅ Async          ✅ Type-safe                 │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                  NOTIFICATION PATTERN (UI alerts)                    │   │
│  │                                                                      │   │
│  │   Any Component ──► NotificationContext ──► NotificationDisplay      │   │
│  │                                                                      │   │
│  │   ✅ Global           ✅ Auto-dismiss          ✅ Priority          │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core Files

FilePurpose
context/mailbox.tsxMailbox provider and message routing
context/notifications.tsxNotification system
services/notifier.tsExternal notification integration
hooks/useMailbox.tsHook for sending/receiving
hooks/useNotification.tsHook for showing notifications

Mailbox System

Architecture

The Mailbox is a message broker that allows any component to send messages to any other component without direct imports.

TypeScript
// context/mailbox.tsx
export type MailboxMessage =
  | { type: 'tool_complete'; toolName: string; result: ToolResult }
  | { type: 'task_started'; taskId: string; description: string }
  | { type: 'task_complete'; taskId: string; status: TaskStatus }
  | { type: 'file_changed'; path: string; change: 'modified' | 'deleted' }
  | { type: 'git_status_changed'; status: GitStatus }
  | { type: 'permission_required'; tool: string; resolve: (allowed: boolean) => void }
  | { type: 'compact_needed'; reason: 'token_limit' | 'user_request' }

// The mailbox is a pub/sub system
type Mailbox = {
  // Send a message (fire-and-forget)
  send: (message: MailboxMessage) => void

  // Subscribe to messages
  subscribe: (
    filter: (msg: MailboxMessage) => boolean,
    handler: (msg: MailboxMessage) => void
  ) => () => void  // Returns unsubscribe

  // Request/response pattern
  request: <T extends MailboxMessage>(
    message: T,
    timeout?: number
  ) => Promise<T['response']>
}
Implementation
TypeScript
// context/mailbox.tsx
const MailboxContext = createContext<Mailbox | null>(null)

export function MailboxProvider({ children }: { children: ReactNode }) {
  // Message bus
  const subscribers = useRef(new Set<{
    filter: (msg: MailboxMessage) => boolean
    handler: (msg: MailboxMessage) => void
  }>())

  const mailbox: Mailbox = useMemo(() => ({
    send: (message) => {
      // Notify all matching subscribers
      subscribers.current.forEach(sub => {
        if (sub.filter(message)) {
          sub.handler(message)
        }
      })
    },

    subscribe: (filter, handler) => {
      const subscription = { filter, handler }
      subscribers.current.add(subscription)

      // Return unsubscribe function
      return () => {
        subscribers.current.delete(subscription)
      }
    },

    request: async (message, timeout = 30000) => {
      return new Promise((resolve, reject) => {
        const requestId = generateUUID()
        const responseHandler = (response: unknown) => {
          clearTimeout(timer)
          resolve(response as T['response'])
        }

        const timer = setTimeout(() => {
          reject(new Error('Mailbox request timeout'))
        }, timeout)

        // Send request with callback
        mailbox.send({
          ...message,
          _requestId: requestId,
          _respond: responseHandler,
        } as MailboxMessage)
      })
    },
  }), [])

  return (
    <MailboxContext.Provider value={mailbox}>
      {children}
    </MailboxContext.Provider>
  )
}
Usage Patterns
Pattern 1: Tool → UI Updates
TypeScript
// tools/BashTool/BashTool.ts
export const BashTool: Tool = {
  async execute(input, context) {
    const { mailbox } = context

    // Notify that task started
    mailbox?.send({
      type: 'task_started',
      taskId: input.taskId,
      description: `Running: ${input.command}`,
    })

    // Execute...
    const result = await runCommand(input.command)

    // Notify completion
    mailbox?.send({
      type: 'task_complete',
      taskId: input.taskId,
      status: result.exitCode === 0 ? 'completed' : 'failed',
    })

    return result
  },
}
Pattern 2: UI Subscribing to Events
TypeScript
// components/TaskPanel.tsx
export function TaskPanel() {
  const [tasks, setTasks] = useState<Task[]>([])
  const mailbox = useMailbox()

  useEffect(() => {
    // Subscribe to task-related messages
    const unsubscribe = mailbox.subscribe(
      msg => msg.type === 'task_started' || msg.type === 'task_complete',
      msg => {
        if (msg.type === 'task_started') {
          setTasks(prev => [...prev, {
            id: msg.taskId,
            description: msg.description,
            status: 'running',
          }])
        } else if (msg.type === 'task_complete') {
          setTasks(prev =>
            prev.map(t =>
              t.id === msg.taskId
                ? { ...t, status: msg.status }
                : t
            )
          )
        }
      }
    )

    return unsubscribe
  }, [mailbox])

  return <TaskList tasks={tasks} />
}
Pattern 3: Request/Response
TypeScript
// Permission request via mailbox
async function requestPermissionViaMailbox(
  tool: string,
  input: unknown
): Promise<boolean> {
  const mailbox = useMailbox()

  try {
    const result = await mailbox.request({
      type: 'permission_required',
      tool,
      input,
    }, 60000)  // 60 second timeout

    return result.allowed
  } catch (e) {
    // Timeout or error = deny
    return false
  }
}

// Permission dialog subscribes and responds
export function PermissionDialog() {
  const mailbox = useMailbox()

  useEffect(() => {
    return mailbox.subscribe(
      msg => msg.type === 'permission_required',
      msg => {
        // Show dialog
        showDialog({
          tool: msg.tool,
          onDecision: (allowed) => {
            // Send response back
            msg._respond({ allowed })
          },
        })
      }
    )
  }, [])

  return null
}

Notification System

Architecture

Notifications are transient UI alerts that don't require action but inform the user.

TypeScript
// context/notifications.tsx
export type Notification = {
  id: string
  type: 'info' | 'success' | 'warning' | 'error'
  title: string
  message?: string

  // Auto-dismiss after timeout (ms)
  // undefined = persistent until dismissed
  timeout?: number

  // Actions (optional buttons)
  actions?: Array<{
    label: string
    onPress: () => void
  }>

  // Callbacks
  onDismiss?: () => void
}

type NotificationContext = {
  show: (notification: Omit<Notification, 'id'>) => string  // Returns id
  dismiss: (id: string) => void
  update: (id: string, updates: Partial<Notification>) => void
}
Implementation
TypeScript
// context/notifications.tsx
export function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState<Notification[]>([])

  const context = useMemo(() => ({
    show: (notification) => {
      const id = generateUUID()

      setNotifications(prev => [...prev, { ...notification, id }])

      // Auto-dismiss if timeout specified
      if (notification.timeout) {
        setTimeout(() => {
          dismiss(id)
        }, notification.timeout)
      }

      return id
    },

    dismiss: (id) => {
      setNotifications(prev => {
        const notification = prev.find(n => n.id === id)
        notification?.onDismiss?.()
        return prev.filter(n => n.id !== id)
      })
    },

    update: (id, updates) => {
      setNotifications(prev =>
        prev.map(n =>
          n.id === id ? { ...n, ...updates } : n
        )
      )
    },
  }), [])

  return (
    <NotificationContext.Provider value={context}>
      {children}
      <NotificationDisplay notifications={notifications} />
    </NotificationContext.Provider>
  )
}
Notification Display
TypeScript
// components/NotificationDisplay.tsx
function NotificationDisplay({
  notifications,
}: {
  notifications: Notification[]
}) {
  // Stack notifications at bottom-right
  return (
    <Box position="absolute" bottom={1} right={1} flexDirection="column">
      {notifications.map(notification => (
        <NotificationCard
          key={notification.id}
          notification={notification}
        />
      ))}
    </Box>
  )
}

function NotificationCard({ notification }: { notification: Notification }) {
  const color = {
    info: 'blue',
    success: 'green',
    warning: 'yellow',
    error: 'red',
  }[notification.type]

  return (
    <Box
      borderStyle="round"
      borderColor={color}
      padding={1}
      marginY={1}
    >
      <Text bold color={color}>
        {notification.type === 'error' && '✗ '}
        {notification.type === 'success' && '✓ '}
        {notification.type === 'warning' && '⚠ '}
        {notification.title}
      </Text>

      {notification.message && (
        <Text>{notification.message}</Text>
      )}

      {notification.actions && (
        <Box>
          {notification.actions.map(action => (
            <Button key={action.label} onPress={action.onPress}>
              {action.label}
            </Button>
          ))}
        </Box>
      )}
    </Box>
  )
}

Advanced Patterns

Pattern: Notification Lifecycle
TypeScript
// Show a notification that updates with progress
function showProgressNotification(mailbox: Mailbox) {
  const id = showNotification({
    type: 'info',
    title: 'Uploading files...',
    timeout: undefined,  // Persistent
  })

  // Update as progress happens
  mailbox.subscribe(
    msg => msg.type === 'upload_progress',
    msg => {
      updateNotification(id, {
        title: `Uploading... ${msg.percent}%`,
      })
    }
  )

  // Remove when done
  mailbox.subscribe(
    msg => msg.type === 'upload_complete',
    () => {
      updateNotification(id, {
        type: 'success',
        title: 'Upload complete!',
        timeout: 3000,
      })
    }
  )
}
Pattern: Cross-Window Communication (Bridge)
TypeScript
// Bridge uses mailbox for remote → local communication
export function initBridgeMailbox() {
  const mailbox = useMailbox()

  // Remote commands come via WebSocket
  websocket.onMessage((data) => {
    if (data.type === 'remote_command') {
      // Inject into local mailbox
      mailbox.send({
        type: 'remote_command',
        command: data.command,
        source: 'bridge',
      })
    }
  })

  // Local → Remote
  return mailbox.subscribe(
    msg => ['tool_complete', 'task_complete'].includes(msg.type),
    msg => {
      websocket.send({
        type: 'local_event',
        payload: msg,
      })
    }
  )
}

Key Design Principles

  1. Fire-and-Forget: Most messages don't need responses
  2. Type Safety: MailboxMessage union provides exhaustive checking
  3. Cleanup: All subscriptions return unsubscribe functions
  4. Timeouts: Request/response pattern prevents hanging
  5. Decoupling: Components don't know about each other

Debugging Mailbox

Bash
# Log all mailbox messages
DEBUG=mailbox claude

# See notification queue
claude /debug notifications

# Trace message flow
CLAUDE_CODE_TRACE_MAILBOX=1 claude