Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
YouTube Security VideosNutanix advances legacy and AI app management with AMD(01.10.2026 um 16:00 Uhr)
•
YouTube Security VideosPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••••
Videos & KonferenzenPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••
Sicherheitslücken (CVE)USN-8857-1: KCoreAddons vulnerability(01.10.2026 um 12:48 Uhr)
•••
YouTube Security VideosNutanix advances legacy and AI app management with AMD(01.10.2026 um 16:00 Uhr)
•
YouTube Security VideosPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••••
Videos & KonferenzenPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••
Sicherheitslücken (CVE)USN-8857-1: KCoreAddons vulnerability(01.10.2026 um 12:48 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

Building a TUI with Pieces SDK - Part 2: UI Components

Building a Pieces Copilot TUI - Part 2: Basic UI Components Note: This tutorial is part of the Pieces CLI project. We welcome contributions! Feel free to open…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!




Building a Pieces Copilot TUI - Part 2: Basic UI Components



Note: This tutorial is part of the Pieces CLI project. We welcome contributions! Feel free to open issues, submit PRs, or suggest improvements.






Introduction



Welcome to Part 2! In Part 1, we learned how to use the Pieces OS SDK. Now, we'll start building the Terminal User Interface (TUI) using Textual.



What we'll build in Part 2:




  • Understanding Textual fundamentals

  • Message display widget

  • Chat panel for conversations

  • User input widget

  • A working basic chat interface



What we'll add in Part 3:




  • Conversations list sidebar

  • Streaming handler integration

  • Full view orchestration

  • Testing & optimization









Understanding Textual



Textual is a modern Python framework for building TUIs. It uses a reactive, component-based architecture similar to React.






Project Structure






pieces-copilot-tui/
├── venv/ # Virtual environment
├── requirements.txt # Dependencies
├── run.py # Entry point
└── src/ # Source code
└── pieces_copilot_tui/ # Main package
├── __init__.py # Package initialization
├── app.py # Main TUI application
├── chat_message.py # Individual message widget (Part 2)
├── chat_panel.py # Message display panel (Part 2)
├── chat_input.py # User input widget (Part 2)
├── streaming_handler.py # Pieces SDK streaming logic (Part 3)
├── chats_list.py # Conversations list (Part 3)
└── chat_view.py # Main view orchestrator (Part 3)






In Part 2, we'll build the core UI components. In Part 3, we'll add conversations management and full integration.









Step 1: Create the Message Widget



Let's start with the foundation - src/pieces_copilot_tui/chat_message.py:




# src/pieces_copilot_tui/chat_message.py
"""Chat message widget for displaying individual messages."""

from datetime import datetime
from textual.widgets import Static
from textual.containers import Vertical


class ChatMessage(Vertical):
"""Widget to display a single chat message."""

DEFAULT_CSS = """
ChatMessage {
width: 100%;
height: auto;
padding: 1 2;
margin-bottom: 1;
}

ChatMessage.user-message {
background: $primary 20%;
border-left: thick $primary;
}

ChatMessage.assistant-message {
background: $accent 20%;
border-left: thick $accent;
}

ChatMessage.system-message {
background: $warning 20%;
border-left: thick $warning;
}

ChatMessage .message-header {
color: $text-muted;
text-style: italic;
margin-bottom: 1;
}

ChatMessage .message-content {
color: $text;
}
"""

def __init__(self, role: str, content: str, timestamp: str = None, **kwargs):
super().__init__(**kwargs)
self.role = role
self.content = content
self.timestamp = timestamp or datetime.now().strftime("Today %I:%M %p")

# Set CSS class based on role
self.add_class(f"{role}-message")

def compose(self):
"""Compose the message widget."""
role_emoji = {
"user": "👤",
"assistant": "🤖",
"system": "⚙️"
}
emoji = role_emoji.get(self.role, "")

yield Static(
f"{emoji} {self.role.title()} - {self.timestamp}",
classes="message-header"
)
yield Static(self.content, classes="message-content")






Key concepts:





  • Vertical container stacks children vertically


  • compose() defines child widgets

  • CSS classes are added dynamically based on message role

  • Emojis make it visually appealing! 🎨



CSS Variables:





  • $primary, $accent, $warning - Textual's built-in color variables


  • $text, $text-muted - Text color variables

  • You can customize these in your app's theme









Step 2: Create the Chat Panel



Now src/pieces_copilot_tui/chat_panel.py - the scrollable message display:




# src/pieces_copilot_tui/chat_panel.py
"""Chat panel widget for displaying conversation messages."""

from datetime import datetime
from typing import Optional
from textual.widgets import Static
from textual.containers import VerticalScroll

from .chat_message import ChatMessage


class ChatPanel(VerticalScroll):
"""Panel to display chat messages."""

DEFAULT_CSS = """
ChatPanel {
width: 100%;
height: 1fr;
border: solid $primary;
background: $background;
padding: 1 2;
}

ChatPanel:focus {
border: solid $accent;
}

ChatPanel .thinking-indicator {
color: $warning;
text-style: italic bold blink;
text-align: center;
background: $surface;
border: dashed $warning;
padding: 1;
margin: 1;
}

ChatPanel .streaming-message {
border-left: thick $accent;
text-style: bold;
}

ChatPanel .welcome-message {
text-align: center;
margin: 4 2;
padding: 3;
border: dashed $primary;
background: $primary 10%;
color: $text;
width: 100%;
height: auto;
}
"""

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.border_title = "Chat"
self._messages = []
self._thinking_widget = None
self._streaming_widget = None

def add_message(self, role: str, content: str, timestamp: str = None):
"""Add a new message to the chat panel."""
self._clear_thinking_indicator()

timestamp = timestamp or datetime.now().strftime("Today %I:%M %p")
message = ChatMessage(role, content, timestamp)
self._messages.append(message)
self.mount(message)
self.scroll_end(animate=False)

def add_thinking_indicator(self):
"""Add a thinking indicator."""
self._clear_thinking_indicator()
self._thinking_widget = Static("🤔 Thinking...", classes="thinking-indicator")
self.mount(self._thinking_widget)
self.scroll_end(animate=False)

def add_streaming_message(self, role: str, content: str):
"""Add a streaming message with cursor."""
self._clear_thinking_indicator()

timestamp = datetime.now().strftime("Today %I:%M %p")
self._streaming_widget = ChatMessage(role, content + " ▌", timestamp)
self._streaming_widget.add_class("streaming-message")
self.mount(self._streaming_widget)
self.scroll_end(animate=False)

def update_streaming_message(self, content: str):
"""Update the streaming message content."""
if self._streaming_widget:
# Update the content of the last Static widget in the streaming message
content_widget = self._streaming_widget.query(".message-content").first()
if content_widget:
content_widget.update(content + " ▌")
self.scroll_end(animate=False)

def finalize_streaming_message(self):
"""Convert streaming message to permanent message."""
if self._streaming_widget:
content_widget = self._streaming_widget.query(".message-content").first()
if content_widget:
# Remove cursor
current_content = str(content_widget.renderable).replace(" ▌", "")
content_widget.update(current_content)
self._streaming_widget.remove_class("streaming-message")
self._messages.append(self._streaming_widget)
self._streaming_widget = None

def clear_messages(self):
"""Clear all messages from the chat panel."""
self._clear_thinking_indicator()
if self._streaming_widget:
self._streaming_widget.remove()
self._streaming_widget = None

for message in self._messages:
if message.is_mounted:
message.remove()
self._messages.clear()

def show_welcome(self):
"""Show welcome message."""
welcome_text = """🎯 Pieces Copilot TUI

Type your message below to start chatting!

Press Ctrl+Q to quit.
"""

welcome = Static(welcome_text, classes="welcome-message")
self.mount(welcome)

def _clear_thinking_indicator(self):
"""Remove thinking indicator if present."""
if self._thinking_widget:
if self._thinking_widget.is_mounted:
self._thinking_widget.remove()
self._thinking_widget = None

def is_streaming_active(self) -> bool:
"""Check if streaming or thinking is currently active."""
return self._streaming_widget is not None or self._thinking_widget is not None






Key features:





  • VerticalScroll makes it scrollable

  • Thinking indicator shows "🤔 Thinking..."

  • Streaming shows content + cursor (" ▌")

  • Auto-scrolls to bottom on new messages

  • Welcome message for first-time users









Step 3: Create the Input Widget



Simple but crucial - src/pieces_copilot_tui/chat_input.py:




# src/pieces_copilot_tui/chat_input.py
"""Chat input widget for user message entry."""

from textual.widgets import Input
from textual.message import Message


class MessageSubmitted(Message):
"""Message emitted when user submits input."""

def __init__(self, text: str):
super().__init__()
self.text = text


class ChatInput(Input):
"""Input widget for chat messages."""

DEFAULT_CSS = """
ChatInput {
width: 100%;
height: 3;
background: $surface;
border: solid $primary;
padding: 0 1;
margin: 0;
dock: bottom;
}

ChatInput:focus {
border: solid $accent;
background: $panel;
}
"""

def __init__(self, **kwargs):
super().__init__(placeholder="Type your message here...", **kwargs)

async def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission."""
if event.value.strip():
self.post_message(MessageSubmitted(event.value.strip()))
self.value = ""
event.stop()

def on_mount(self) -> None:
"""Focus the input when mounted."""
self.focus()






Textual messages:




  • Custom MessageSubmitted message bubbles up to parent

  • Parent widgets can listen for this message


  • event.stop() prevents further propagation


  • post_message() sends the message up the widget tree



How it works:




  1. User types message and presses Enter


  2. on_input_submitted() is called

  3. Creates MessageSubmitted message with the text

  4. Parent widget receives it via on_message_submitted()

  5. Input is cleared for next message









Step 4: Create a Simple View (For Testing)



Let's create a basic src/pieces_copilot_tui/chat_view.py to test our components:




# src/pieces_copilot_tui/chat_view.py
"""Simple chat view for testing (will be expanded in Part 3)."""

from textual.screen import Screen
from textual.containers import Vertical
from textual.binding import Binding
from textual.widgets import Footer

from pieces_os_client.wrapper import PiecesClient

from .chat_panel import ChatPanel
from .chat_input import ChatInput, MessageSubmitted


class SimpleChatView(Screen):
"""Simple chat view for testing our components."""

BINDINGS = [
Binding("ctrl+q", "quit", "Quit"),
]

DEFAULT_CSS = """
SimpleChatView {
layout: vertical;
}

SimpleChatView Vertical {
width: 100%;
height: 100%;
}
"""

def __init__(self, pieces_client: PiecesClient, **kwargs):
super().__init__(**kwargs)
self.pieces_client = pieces_client
self.chat_panel = None
self.chat_input = None

def compose(self):
"""Compose the view."""
with Vertical():
self.chat_panel = ChatPanel()
yield self.chat_panel

self.chat_input = ChatInput()
yield self.chat_input

yield Footer()

def on_mount(self) -> None:
"""Initialize the view."""
# Show welcome message
self.chat_panel.show_welcome()

def on_message_submitted(self, message: MessageSubmitted) -> None:
"""Handle user message submission."""
# Add user message
self.chat_panel.add_message("user", message.text)

# Show thinking indicator
self.chat_panel.add_thinking_indicator()

# In Part 3, we'll connect this to the streaming handler
# For now, just simulate a response
self.simulate_response(message.text)

def simulate_response(self, user_message: str):
"""Simulate a response (will be replaced with real streaming in Part 3)."""
import asyncio

async def add_response():
await asyncio.sleep(1)
self.chat_panel.add_message(
"assistant",
f"Echo: {user_message}\n\n(Real AI responses coming in Part 3!)"
)

asyncio.create_task(add_response())






💡 Note: This is a simplified version for testing. In Part 3, we'll replace simulate_response() with real streaming from Pieces OS and add the conversations list sidebar.









Step 5: Create the Main Application



Now let's create src/pieces_copilot_tui/app.py:




# src/pieces_copilot_tui/app.py
"""Pieces Copilot TUI - A Terminal User Interface for Pieces OS."""

from textual.app import App

from pieces_os_client.wrapper import PiecesClient
from .chat_view import SimpleChatView


class PiecesCopilotTUI(App):
"""Pieces Copilot TUI (Basic version for Part 2)."""

# CSS styles for the entire app
DEFAULT_CSS = """
Screen {
background: $background;
color: $text;
}

.error {
color: $error;
text-style: bold;
}

.success {
color: $success;
text-style: bold;
}
"""

def __init__(self, pieces_client: PiecesClient = None, **kwargs):
super().__init__(**kwargs)
self.pieces_client = pieces_client or PiecesClient()
self.chat_view = None

def on_mount(self) -> None:
"""Initialize the application."""
self.title = "Pieces Copilot TUI"

# Initialize Pieces client
if not self.pieces_client.connect_websocket():
self.notify("Failed to connect to Pieces OS", severity="error")
self.exit()
return

# Create and push the simple chat view
self.chat_view = SimpleChatView(pieces_client=self.pieces_client)
self.push_screen(self.chat_view)


def run_tui():
"""Run the Pieces Copilot TUI application."""
# Initialize Pieces client
pieces_client = PiecesClient()

# Check if Pieces OS is running
if not pieces_client.is_pieces_running():
print("Error: Pieces OS is not running. Please start Pieces OS first.")
return

app = PiecesCopilotTUI(pieces_client=pieces_client)
app.run()


if __name__ == "__main__":
run_tui()






Key points:




  • We inject PiecesClient for better testability

  • CSS is used for styling (similar to web CSS!)


  • on_mount() is called when the app starts

  • Error handling for Pieces OS connection









Step 6: Create the Package Initialization



Create src/pieces_copilot_tui/__init__.py to make it a proper Python package:




# src/pieces_copilot_tui/__init__.py
"""Pieces Copilot TUI Package."""

__version__ = "0.1.0"

from .app import PiecesCopilotTUI, run_tui

__all__ = ["PiecesCopilotTUI", "run_tui"]






This allows the relative imports (.chat_message, .chat_panel, etc.) to work correctly and exports the main functions.









Step 7: Create the Module Entry Point



Create src/pieces_copilot_tui/__main__.py to make the package executable:




# src/pieces_copilot_tui/__main__.py
"""
Command-line entry point for Pieces Copilot TUI.

This allows the package to be run with: python -m pieces_copilot_tui
"""

from .app import run_tui

if __name__ == "__main__":
run_tui()






This is the Pythonic way to make a package runnable with python -m package_name.









Step 8: Run Your Basic TUI



The cleanest way to run the TUI is from the src directory:




# Navigate to src directory
cd src

# Run the module
python -m pieces_copilot_tui






That's it! No complex setup, no PYTHONPATH manipulation, just clean Python!



What you should see:




  • A welcome message in the center

  • An input box at the bottom

  • You can type messages and see them displayed

  • Simulated responses after 1 second

  • Press Ctrl+Q to quit









Recap



In Part 2, we built the foundation of our TUI:






Files Created




  1. ✅ src/pieces_copilot_tui/chat_message.py - Individual message widget

  2. ✅ src/pieces_copilot_tui/chat_panel.py - Scrollable message display

  3. ✅ src/pieces_copilot_tui/chat_input.py - User input widget

  4. ✅ src/pieces_copilot_tui/chat_view.py - Simple view (basic version)

  5. ✅ src/pieces_copilot_tui/app.py - Main application

  6. ✅ src/pieces_copilot_tui/__init__.py - Package definition

  7. ✅ src/pieces_copilot_tui/__main__.py - Module entry point









What's Next in Part 3?



In Part 3: Advanced Features & Integration, we'll add:




  1. 📋 Conversations List - Sidebar showing all your chats

  2. 🌊 Real Streaming - Connect to Pieces OS for actual AI responses

  3. 🎯 Full Orchestration - Complete chat_view.py with all features

  4. 🧠 LTM Support - Long-Term Memory toggle

  5. ⚡ Advanced Features - Delete, rename, create conversations

  6. 🧪 Testing & Optimization - Make it production-ready



Preview of Part 3 components:





  • chats_list.py - Full conversations management


  • streaming_handler.py - Real-time streaming from Pieces


  • chat_view.py - Complete orchestrator (expanded version)

  • Testing, debugging, and performance tips









Troubleshooting






Textual Not Found






pip install textual>=0.47.0









UI Not Updating






# Make sure you're using self.mount() to add widgets
self.mount(message)

# Not just appending to a list












Ready for Part 3?



You now have a working TUI foundation! In Part 3, we'll transform this into a fully-featured Pieces Copilot interface with real AI streaming, conversations management, and all the bells and whistles.



Continue to Part 3 when you're ready! 🚀

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

Bedrohungsgraph · ATT&CK-Mapping · Exploit-Belege
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
2 Knoten · 1 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a TUI with Pieces SDK - Part 2: UI Components

Thematisch verwandte Begriffe: Building, with, Pieces, Part · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag