Skip to content

Implement Static component integration for completed messages #11768

Closed as not planned

Description

@randomm

Parent Epic

Part of #11762 (Ink-based TUI Rewrite)

Objective

Use Ink's <Static> component to render completed messages permanently above the dynamic area. This is the key to solving the streaming + live updates problem.

How Static Works

import { Static, Box, Text } from 'ink'

const App = () => {
  const [messages, setMessages] = useState<Message[]>([])
  
  return (
    <Box flexDirection="column">
      {/* Completed messages - rendered once, never re-rendered */}
      <Static items={messages}>
        {(message) => (
          <Box key={message.id} flexDirection="column">
            <UserPrompt text={message.prompt} />
            <AssistantResponse 
              text={message.text}
              tools={message.tools}
              tasks={message.tasks}
            />
          </Box>
        )}
      </Static>
      
      {/* Dynamic area - updates frequently */}
      <LiveSection />
    </Box>
  )
}

Why This Solves Our Problem

  1. Completed messages are written to stdout ONCE and stay there
  2. Dynamic area (streaming text, active tools) re-renders below
  3. No conflicts between "frozen" and "active" content
  4. Terminal scrollback preserved naturally

Tasks

  • Create src/cli/ink/components/Message.tsx - wrapper for completed message
  • Create src/cli/ink/components/UserPrompt.tsx - displays user input
  • Create src/cli/ink/components/AssistantResponse.tsx - displays response with tools
  • Integrate Static in App.tsx
  • Handle message completion transition:
    • When streaming ends, dispatch MESSAGE_COMPLETE
    • Move current streaming state to messages array
    • Static picks up new message, renders permanently
  • Test with multiple message exchanges

Component Structure

<Static items={messages}>
  <Message>
    <UserPrompt>
      "project-manager ❯ /start"
    </UserPrompt>
    <AssistantResponse>
      <ProseBlock text="I'll initialize..." />
      <ToolList>
        <ToolDisplay name="bash" summary="remory search..." status="done" />
        <TaskDisplay agent="git-agent" description="Get state" status="done" />
      </ToolList>
    </AssistantResponse>
  </Message>
</Static>

Acceptance Criteria

  • Completed messages render permanently
  • New messages append below existing
  • Dynamic area stays at bottom
  • Terminal scrollback works correctly
  • No duplicate rendering of completed content

Estimated Effort

1 day

Risk

Medium - Understanding Static's behavior is crucial

Dependencies

Activity

  1. added
    opentuiThis relates to changes in v1.0, now that opencode uses opentui
    on Feb 2, 2026
  2. randomm commented on Feb 2, 2026

    @randomm
    Author

    Research: Ink Static Component

    Key Pattern:

    <Static items={completedMessages}>
      {msg => <Message key={msg.id} {...msg} />}
    </Static>
    <CurrentMessage /> {/* Live updates below */}

    Critical Gotchas:

    • Only NEW items trigger re-renders - modifications to existing items are IGNORED
    • Must use immutable append: setItems(prev => [...prev, newItem])
    • Each item needs stable key prop

    Implementation Approach:

    1. Store completed messages in separate state array
    2. When message completes, append to completed array (never modify)
    3. Render completed in <Static>, current message below
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

opentuiThis relates to changes in v1.0, now that opencode uses opentui

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions