Implement useSDKEvents hook for event stream integration #8316

Closed
opened 2026-02-16 18:09:40 -05:00 by yindo · 2 comments
Owner

Originally created by @randomm on GitHub (Feb 2, 2026).

Originally assigned to: @thdxr on GitHub.

Parent Epic

Part of #11762 (Ink-based TUI Rewrite)

Objective

Create React hook that subscribes to SDK events and dispatches state updates. This is the critical integration point between the backend and UI.

Architecture

function useSDKEvents(sessionId: string | null, dispatch: Dispatch<Action>) {
  useEffect(() => {
    if (!sessionId) return
    
    const abortController = new AbortController()
    
    ;(async () => {
      const client = createOpencodeClient({
        baseURL: `http://localhost:${port}`,
        // auth headers...
      })
      
      const events = await client.event.subscribe({
        signal: abortController.signal
      })
      
      for await (const event of events.stream) {
        // Dispatch actions based on event type
        switch (event.type) {
          case 'part.updated':
            handlePartUpdated(event, dispatch)
            break
          case 'message.created':
            handleMessageCreated(event, dispatch)
            break
          // etc.
        }
      }
    })()
    
    return () => abortController.abort()
  }, [sessionId, dispatch])
}

Event Types to Handle

SDK Event Action Purpose
part.updated (tool) TOOL_START / TOOL_END Track tool status
part.updated (task) TASK_START / TASK_UPDATE_CHILD / TASK_END Track task + child
message.created MESSAGE_COMPLETE Finalize message
session.updated SET_SESSION Session metadata

Child Session Tracking

The hardest part - mapping child session events to parent tasks:

function handlePartUpdated(event: PartUpdatedEvent, dispatch: Dispatch<Action>) {
  const part = event.properties.part
  
  // If this is a task tool, extract child session ID from metadata
  if (part.tool === 'task' && part.state.metadata?.sessionId) {
    dispatch({
      type: 'TASK_REGISTER_CHILD_SESSION',
      taskId: part.callID,
      childSessionId: part.state.metadata.sessionId
    })
  }
  
  // If this is from a child session, find parent task
  if (part.sessionID !== currentSessionId) {
    const parentTaskId = childSessionToTask.get(part.sessionID)
    if (parentTaskId) {
      dispatch({
        type: 'TASK_UPDATE_CHILD',
        taskId: parentTaskId,
        childTool: {
          name: part.tool,
          summary: part.state.title || 'running',
          status: part.state.status
        }
      })
    }
  }
}

Tasks

  • Create src/cli/ink/hooks/useSDKEvents.ts
  • Implement SDK client setup with auth
  • Implement event stream subscription
  • Handle tool part updates
  • Handle task part updates with child session mapping
  • Handle message completion
  • Implement cleanup on unmount
  • Handle reconnection on disconnect
  • Add error handling for stream failures

State Maps Needed

// In reducer or context
const childSessionToTask = new Map<string, string>()  // child session ID -> task call ID
const taskToChildSession = new Map<string, string>()  // task call ID -> child session ID

Acceptance Criteria

  • Events flow from SDK to UI state
  • Tools display and update correctly
  • Tasks display with child tool status
  • Stream reconnects on failure
  • Cleanup happens on unmount

Estimated Effort

2.5 days

Risk

HIGH - This is the core complexity. The SDK event handling is the same regardless of UI framework.

Dependencies

  • #11763 (Setup Ink dependencies)
  • #11764 (App component with reducer)
Originally created by @randomm on GitHub (Feb 2, 2026). Originally assigned to: @thdxr on GitHub. ## Parent Epic Part of #11762 (Ink-based TUI Rewrite) ## Objective Create React hook that subscribes to SDK events and dispatches state updates. This is the critical integration point between the backend and UI. ## Architecture ```tsx function useSDKEvents(sessionId: string | null, dispatch: Dispatch<Action>) { useEffect(() => { if (!sessionId) return const abortController = new AbortController() ;(async () => { const client = createOpencodeClient({ baseURL: `http://localhost:${port}`, // auth headers... }) const events = await client.event.subscribe({ signal: abortController.signal }) for await (const event of events.stream) { // Dispatch actions based on event type switch (event.type) { case 'part.updated': handlePartUpdated(event, dispatch) break case 'message.created': handleMessageCreated(event, dispatch) break // etc. } } })() return () => abortController.abort() }, [sessionId, dispatch]) } ``` ## Event Types to Handle | SDK Event | Action | Purpose | |-----------|--------|---------| | `part.updated` (tool) | `TOOL_START` / `TOOL_END` | Track tool status | | `part.updated` (task) | `TASK_START` / `TASK_UPDATE_CHILD` / `TASK_END` | Track task + child | | `message.created` | `MESSAGE_COMPLETE` | Finalize message | | `session.updated` | `SET_SESSION` | Session metadata | ## Child Session Tracking The hardest part - mapping child session events to parent tasks: ```tsx function handlePartUpdated(event: PartUpdatedEvent, dispatch: Dispatch<Action>) { const part = event.properties.part // If this is a task tool, extract child session ID from metadata if (part.tool === 'task' && part.state.metadata?.sessionId) { dispatch({ type: 'TASK_REGISTER_CHILD_SESSION', taskId: part.callID, childSessionId: part.state.metadata.sessionId }) } // If this is from a child session, find parent task if (part.sessionID !== currentSessionId) { const parentTaskId = childSessionToTask.get(part.sessionID) if (parentTaskId) { dispatch({ type: 'TASK_UPDATE_CHILD', taskId: parentTaskId, childTool: { name: part.tool, summary: part.state.title || 'running', status: part.state.status } }) } } } ``` ## Tasks - [ ] Create `src/cli/ink/hooks/useSDKEvents.ts` - [ ] Implement SDK client setup with auth - [ ] Implement event stream subscription - [ ] Handle tool part updates - [ ] Handle task part updates with child session mapping - [ ] Handle message completion - [ ] Implement cleanup on unmount - [ ] Handle reconnection on disconnect - [ ] Add error handling for stream failures ## State Maps Needed ```tsx // In reducer or context const childSessionToTask = new Map<string, string>() // child session ID -> task call ID const taskToChildSession = new Map<string, string>() // task call ID -> child session ID ``` ## Acceptance Criteria - [ ] Events flow from SDK to UI state - [ ] Tools display and update correctly - [ ] Tasks display with child tool status - [ ] Stream reconnects on failure - [ ] Cleanup happens on unmount ## Estimated Effort 2.5 days ## Risk HIGH - This is the core complexity. The SDK event handling is the same regardless of UI framework. ## Dependencies - #11763 (Setup Ink dependencies) - #11764 (App component with reducer)
yindo closed this issue 2026-02-16 18:09:40 -05:00
Author
Owner

@randomm commented on GitHub (Feb 2, 2026):

SDK Event Types Research

Source

/packages/sdk/js/src/v2/gen/types.gen.ts

Core Events for TUI

Event Purpose UI Component
message.part.updated Streaming text/tool/reasoning StreamingProse, ToolDisplay
session.status idle/retry/busy StatusBar
session.error Error handling Error display
question.asked User prompts Dialog overlays
permission.asked Permission requests Permission dialog
tui.toast.show Notifications Toast component

Message Part Types (discriminated union)

  • text - Streaming content with optional delta
  • tool - Tool with state: pending|running|completed|error
  • reasoning - AI thinking
  • subtask - Subagent tasks
  • step-start / step-finish - Step markers

Implementation Notes

  • Use discriminated union pattern for part type handling
  • delta field enables incremental text updates (efficient)
  • All TUI events have corresponding SDK events - no gaps found
@randomm commented on GitHub (Feb 2, 2026): ## SDK Event Types Research ### Source `/packages/sdk/js/src/v2/gen/types.gen.ts` ### Core Events for TUI | Event | Purpose | UI Component | |-------|---------|--------------| | `message.part.updated` | Streaming text/tool/reasoning | StreamingProse, ToolDisplay | | `session.status` | idle/retry/busy | StatusBar | | `session.error` | Error handling | Error display | | `question.asked` | User prompts | Dialog overlays | | `permission.asked` | Permission requests | Permission dialog | | `tui.toast.show` | Notifications | Toast component | ### Message Part Types (discriminated union) - `text` - Streaming content with optional `delta` - `tool` - Tool with state: `pending`|`running`|`completed`|`error` - `reasoning` - AI thinking - `subtask` - Subagent tasks - `step-start` / `step-finish` - Step markers ### Implementation Notes - Use discriminated union pattern for part type handling - `delta` field enables incremental text updates (efficient) - All TUI events have corresponding SDK events - no gaps found
Author
Owner

@randomm commented on GitHub (Feb 3, 2026):

Reopening - Phase 1 & 2 components merged successfully. Proceeding with hook implementation.

@randomm commented on GitHub (Feb 3, 2026): Reopening - Phase 1 & 2 components merged successfully. Proceeding with hook implementation.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#8316