Awesome-omni-skill copilot-tui-harness
Expert in the Copilot SDK TUI Harness project. Use for development tasks including architecture, event system, plugins, OpenTUI components, and Copilot SDK integration. Triggers on TUI development, harness events, streaming UI, plugin system, event-driven architecture.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/copilot-tui-harness" ~/.claude/skills/diegosouzapw-awesome-omni-skill-copilot-tui-harness && rm -rf "$T"
skills/development/copilot-tui-harness/SKILL.mdCopilot SDK TUI Harness Development Skill
Expert skill for developing the Copilot SDK TUI Harness—a terminal user interface that wraps the GitHub Copilot SDK with an event-driven, plugin-based architecture.
Project Identity
Name: Copilot SDK TUI Harness (copilot-anvil)
Stack: TypeScript + OpenTUI/React + @github/copilot-sdk
Runtime: Bun 1.0+
Architecture: Event-driven with strict UI ↔ SDK decoupling
Core Principles
1. Strict Layer Separation
UI Layer (OpenTUI/React) ↕ HarnessEvent / UIAction Harness (Orchestrator) ↕ Internal callbacks CopilotSessionAdapter ↕ @github/copilot-sdk
CRITICAL: The UI layer never imports
@github/copilot-sdk directly. All SDK interaction flows through the adapter and harness layers.
2. Event-Driven Communication
- Harness → UI:
types (run.started, assistant.delta, log, etc.)HarnessEvent - UI → Harness:
types (submit.prompt, cancel)UIAction - No direct function calls between layers—everything flows through events
3. Plugin-Ready Architecture
The system is designed for extensibility via plugins that can register:
- Tools (callable by the assistant)
- Commands (user-invokable actions)
- Panes (UI components)
- State slices (additional state management)
Architecture Deep Dive
Directory Structure
src/ ├── index.tsx # Entry point, bootstraps app ├── copilot/ │ └── CopilotSessionAdapter.ts # ONLY place that imports SDK ├── harness/ │ ├── Harness.ts # Orchestrator, state manager │ ├── events.ts # Event type definitions │ └── plugins.ts # Plugin system interfaces ├── ui/ │ ├── App.tsx # Main OpenTUI app component │ ├── theme.ts # Theme configuration │ ├── syntaxTheme.ts # Syntax highlighting theme │ └── panes/ │ ├── ChatPane.tsx # Conversation transcript │ ├── InputBar.tsx # Prompt input with image support │ ├── Sidebar.tsx # Sidebar container │ ├── TasksPane.tsx # Task tracking display │ ├── ContextPane.tsx # Context information │ ├── SubagentsPane.tsx # Subagent monitoring │ ├── FilesModifiedPane.tsx # Git modified files │ ├── PlanPane.tsx # Plan viewer │ ├── LogsPane.tsx # System logs │ ├── StartScreen.tsx # Welcome screen │ ├── ModelSelector.tsx # Model selection modal │ ├── SessionSwitcher.tsx # Session management │ ├── SkillsPane.tsx # Skills browser │ ├── QuestionModal.tsx # Interactive questions │ ├── ConfirmModal.tsx # Confirmation dialogs │ ├── CommandModal.tsx # Ephemeral run display │ └── DebugOverlay.tsx # Debug overlay ├── commands/ # Slash command system │ └── CommandLoader.ts # Command registry └── utils/ # Shared utilities ├── git.ts # Git operations ├── gitDiff.ts # Diff utilities └── stderrCapture.ts # Error capture
Key Files and Responsibilities
src/copilot/CopilotSessionAdapter.ts
src/copilot/CopilotSessionAdapter.tsPurpose: Wraps Copilot SDK, translates SDK events to HarnessEvents
Responsibilities:
- Initialize CopilotClient
- Create/manage CopilotSession (with
)streaming: true - Subscribe to SDK events and emit corresponding HarnessEvents
- Handle prompt submission via session.sendAndWait()
- Must NOT contain UI code or import UI components
Key Pattern:
// SDK event → Harness event translation session.on('assistant.message_delta', (event) => { this.emit({ type: 'assistant.delta', runId: this.currentRunId, text: event.content }); });
src/harness/Harness.ts
src/harness/Harness.tsPurpose: Central orchestrator and state manager
Responsibilities:
- Maintain UI transcript (for display only—SDK has true memory)
- Own event bus and lifecycle state
- Handle UIAction dispatch from UI
- Support plugin registration and management
- Coordinate between adapter and UI
Key Patterns:
- State is immutable; updates create new state objects
- Events are emitted synchronously to all listeners
- Plugins hook into lifecycle via
onEvent()
src/harness/events.ts
src/harness/events.tsPurpose: Define all event and action types
Core HarnessEvent Types:
| Event | Payload | When Emitted |
|---|---|---|
| | Prompt submitted, SDK begins processing |
| | Streaming token received from SDK |
| | Complete assistant response ready |
| | Reasoning token received (o1 models) |
| | Complete reasoning content |
| | Run completed successfully |
| | User cancelled with Ctrl+C |
| | Tool execution begins |
| | Tool progress update |
| | Tool execution completes |
| | Subagent invoked |
| | Subagent finished |
| | Subagent failed |
| | Skill invoked |
| | Agent intent updated |
| | Task list updated |
| | Plan content updated |
| | New turn begins |
| | Turn completes |
| | Agent asks question |
| | User answers question |
| | Session changed |
| | New session created |
| | Session list refreshed |
| | AI model changed |
| | Token usage info |
| | Quota info |
| | System or plugin log message |
UIAction Types:
| Action | Payload | Effect |
|---|---|---|
| | Send prompt to Copilot SDK (with optional images) |
| | Abort current run |
| | Switch AI model |
| | Answer agent question |
| | Create new session |
| | Switch to session |
| | Refresh session list |
| | Close ephemeral run modal |
src/harness/plugins.ts
src/harness/plugins.tsPurpose: Define plugin system interfaces
Plugin Interface:
interface HarnessPlugin { name: string; register(ctx: PluginContext): void; onEvent?(event: HarnessEvent): void; } interface PluginContext { tools: ToolRegistry; commands: CommandRegistry; panes: PaneRegistry; state: StateRegistry; emit(event: HarnessEvent): void; }
Key Concept: Plugins receive a context object during registration and can subscribe to all harness events.
src/ui/App.tsx
src/ui/App.tsxPurpose: Main OpenTUI application component
Responsibilities:
- Layout definition (chat + logs + input)
- Subscribe to harness events and update local state
- Dispatch UIActions in response to user input
- Render keybind hints and status bar
Key Patterns:
- Use React hooks (useState, useEffect) for local UI state
- Never directly manipulate harness state
- All harness communication via events/actions
src/ui/panes/ChatPane.tsx
src/ui/panes/ChatPane.tsxPurpose: Display conversation transcript with streaming support
Responsibilities:
- Render user and assistant messages
- Show streaming draft during active run
- Display tool calls inline in transcript
- Handle scrolling and layout
- Support syntax highlighting for code blocks
Key Pattern: Separate committed messages from streaming buffer:
{transcript.map(item => item.kind === "message" ? <Message {...item} /> : <ToolCall {...item} /> )} {streamingBuffer && <StreamingMessage text={streamingBuffer} />}
src/ui/panes/InputBar.tsx
src/ui/panes/InputBar.tsxPurpose: Handle prompt input with image attachment support
Responsibilities:
- Provide text input field
- Dispatch
action on Entersubmit.prompt - Disable input during active run
- Handle image attachment (Ctrl+I)
- Show input hints
src/ui/panes/Sidebar.tsx
src/ui/panes/Sidebar.tsxPurpose: Container for sidebar panels
Responsibilities:
- Layout sidebar panes vertically
- Manage space allocation between panes
- Show/hide panes based on content
src/ui/panes/TasksPane.tsx
src/ui/panes/TasksPane.tsxPurpose: Display active tasks
Responsibilities:
- Show running, completed, and failed tasks
- Display task timing and status
- Update in real-time via events
src/ui/panes/ContextPane.tsx
src/ui/panes/ContextPane.tsxPurpose: Display current context information
Responsibilities:
- Show git information (branch, repo, commit)
- Display active tools
- Show token usage and quota
src/ui/panes/SubagentsPane.tsx
src/ui/panes/SubagentsPane.tsxPurpose: Monitor subagent execution
Responsibilities:
- Display active and completed subagents
- Show subagent status and timing
- Track subagent failures
src/ui/panes/FilesModifiedPane.tsx
src/ui/panes/FilesModifiedPane.tsxPurpose: Show git modified files
Responsibilities:
- List unstaged and staged changes
- Display file change types (modified, added, deleted)
- Update periodically from git status
src/ui/panes/PlanPane.tsx
src/ui/panes/PlanPane.tsxPurpose: Display execution plan
Responsibilities:
- Show current plan content
- Support markdown rendering
- Track plan updates
src/ui/panes/ModelSelector.tsx
src/ui/panes/ModelSelector.tsxPurpose: Model selection modal
Responsibilities:
- Display available models
- Allow model switching
- Show current model
- Handle keyboard navigation
src/ui/panes/SessionSwitcher.tsx
src/ui/panes/SessionSwitcher.tsxPurpose: Session management modal
Responsibilities:
- List all sessions
- Allow session switching
- Support creating new sessions
- Show session metadata
src/ui/panes/SkillsPane.tsx
src/ui/panes/SkillsPane.tsxPurpose: Skills browser
Responsibilities:
- List available skills
- Show skill descriptions
- Allow skill invocation
- Display skill metadata
src/ui/panes/QuestionModal.tsx
src/ui/panes/QuestionModal.tsxPurpose: Interactive user questions
Responsibilities:
- Display agent questions
- Show choices (if provided)
- Support freeform input
- Handle keyboard navigation
src/ui/panes/CommandModal.tsx
src/ui/panes/CommandModal.tsxPurpose: Ephemeral run display
Responsibilities:
- Show ephemeral run output
- Display status and progress
- Allow closing modal
src/ui/panes/StartScreen.tsx
src/ui/panes/StartScreen.tsxPurpose: Welcome screen
Responsibilities:
- Show before first interaction
- Display keybind hints
- Provide getting started info
Event Flow Patterns
Typical Prompt Submission Flow
1. User types in InputBar, presses Enter 2. InputBar dispatches UIAction({ type: "submit.prompt", text }) 3. Harness.dispatch() receives action 4. Harness emits HarnessEvent({ type: "run.started", runId, createdAt }) 5. Harness calls CopilotSessionAdapter.sendPrompt(text) 6. Adapter calls session.sendAndWait({ prompt: text }) 7. SDK streams response tokens 8. For each token: a. Adapter receives SDK event b. Adapter emits HarnessEvent({ type: "assistant.delta", runId, text }) c. Harness updates streaming buffer d. UI re-renders with updated buffer 9. SDK completes 10. Adapter emits HarnessEvent({ type: "assistant.message", runId, message }) 11. Harness commits buffer to transcript 12. Harness emits HarnessEvent({ type: "run.finished", runId, createdAt }) 13. UI shows completed message, re-enables input
Cancellation Flow
1. User presses Ctrl+C during run 2. UI dispatches UIAction({ type: "cancel" }) 3. Harness.dispatch() receives action 4. Harness tells adapter to cancel 5. Adapter stops processing deltas (best-effort) 6. Adapter emits HarnessEvent({ type: "run.cancelled", runId, createdAt }) 7. Harness clears streaming buffer, resets to idle 8. UI re-enables input
Development Guidelines
When Adding a New Feature
1. Define Events First
- Add new event types to
src/harness/events.ts - Define clear contracts (what data, when emitted)
2. Implement in Layers
- If SDK-related: extend
CopilotSessionAdapter - If state/logic: extend
Harness - If UI: add/modify components in
src/ui/
3. Maintain Separation
- Never import SDK in UI
- Keep adapter simple—just translation
- Put logic in harness, not adapter or UI
When Adding a Plugin
1. Define the plugin object:
const myPlugin: HarnessPlugin = { name: "my-feature", register(ctx) { // Register tools, commands, etc. ctx.tools.register("myTool", async (args) => { return { result: "..." }; }); }, onEvent(event) { // React to lifecycle events if (event.type === "run.started") { console.log("Run started!"); } } };
2. Register with harness:
harness.use(myPlugin);
3. Test integration:
- Verify tools are callable by assistant
- Ensure events are received
- Check for conflicts with existing plugins
When Adding a New Pane
1. Create component:
src/ui/panes/NewPane.tsx
2. Design state needs:
- What events does it subscribe to?
- What actions does it dispatch?
- What data does it display?
3. Add to layout: Update
src/ui/App.tsx
- Import the pane
- Add to the appropriate section (main area, sidebar, or modal)
- Connect to harness state
4. Handle keyboard shortcuts (if needed):
- Add keybind in
App.tsx - Update state to show/hide pane
5. (Future) Register via plugin: Use
ctx.panes.register()
Existing panes for reference:
- Main area:
,ChatPaneInputBar - Sidebar:
,TasksPane
,ContextPane
,SubagentsPane
,FilesModifiedPanePlanPane - Modals:
,ModelSelector
,SessionSwitcher
,SkillsPane
,QuestionModal
,ConfirmModalCommandModal - Overlays:
,StartScreenDebugOverlay
When Modifying SDK Integration
ONLY edit src/copilot/CopilotSessionAdapter.ts
Common tasks:
- Change session options (model, streaming, etc.)
- Add new SDK event subscriptions
- Adjust prompt formatting
- Handle new SDK capabilities
Never:
- Import SDK elsewhere
- Put business logic in adapter
- Directly manipulate UI from adapter
Common Patterns
Pattern: Buffered Streaming
// In Harness private streamingBuffer = ""; onDelta(event: AssistantDeltaEvent) { this.streamingBuffer += event.text; this.notifyUI(); } onComplete(event: AssistantMessageEvent) { this.transcript.push({ role: "assistant", content: this.streamingBuffer, createdAt: new Date() }); this.streamingBuffer = ""; this.notifyUI(); }
Pattern: Idempotent Event Handling
// In Plugin onEvent(event: HarnessEvent) { if (event.type === "run.started") { // Always safe to call, even if called multiple times this.resetState(); } }
Pattern: Async Tool Registration
// In Plugin register(ctx: PluginContext) { ctx.tools.register("fetchData", async (args) => { const data = await fetch(args.url); return await data.json(); }); }
Testing Strategies
Manual Testing
bun run dev
Test cases:
- Send a prompt, verify streaming display
- Send multiple prompts, verify session continuity
- Press Ctrl+C during streaming, verify cancellation
- Trigger error (bad auth), verify error handling
- Check logs pane shows lifecycle events
Integration Testing
- Mock CopilotSessionAdapter
- Emit events manually
- Verify harness state updates
- Verify UI renders correctly
Unit Testing
- Test event type definitions
- Test plugin registration logic
- Test utility functions
Debugging
Enable verbose logging
// In Harness or Adapter this.emit({ type: "log", level: "info", message: "Debug: streaming delta", data: { text, runId } });
Check event flow
Add logging to:
— see incoming actionsHarness.dispatch()
event handlers — see SDK eventsAdapter- UI
hooks — see state updatesuseEffect
Inspect state
Use OpenTUI devtools or add temporary UI to display:
- Current harness state
- Active run ID
- Streaming buffer content
- Plugin registry contents
Performance Considerations
Streaming Updates
- Debounce UI updates if delta rate is very high
- Use React memoization for message list
- Limit log pane to last N entries
Event Listeners
- Remove listeners on component unmount
- Use weak references if holding onto event listeners long-term
Plugin Load Time
- Keep
fast—defer heavy init to first useregister() - Don't block on network calls during registration
Common Pitfalls
❌ Importing SDK in UI
// WRONG - never do this import { CopilotClient } from "@github/copilot-sdk";
Fix: Use harness events and actions instead
❌ Putting Logic in Adapter
// WRONG - adapter should not have business logic adapter.sendPrompt = (text) => { if (text.includes("secret")) { // Don't do this here! throw new Error("Forbidden"); } session.sendAndWait({ prompt: text }); }
Fix: Put validation in harness or plugin
❌ Mutating Harness State from UI
// WRONG - never mutate harness state directly harness.state.transcript.push(newMessage);
Fix: Dispatch an action, let harness update its own state
❌ Blocking Event Handlers
// WRONG - don't block the event loop onEvent(event) { if (event.type === "run.started") { // This blocks! const result = syncExpensiveOperation(); } }
Fix: Use async/await or defer work to next tick
Scripts and Commands
| Command | Purpose |
|---|---|
| Start TUI in development mode |
| Same as dev (alias) |
| Install dependencies |
Dependencies
Production
— Core SDK for agent runtime@github/copilot-sdk
— Terminal UI framework (core)@opentui/core
— React reconciler for OpenTUI@opentui/react
— React libraryreact
— Diff utility (future use)diff
Development
— TypeScript compilertypescript
— Node.js type definitions@types/node
— React type definitions@types/react
— Diff type definitions@types/diff
Configuration Files
— Project manifest, scripts, dependenciespackage.json
— TypeScript compiler optionstsconfig.json
— Bun lockfilebun.lock
Resources
- Full requirements:
docs/REQUIREMENTS.md - Agent docs:
AGENTS.md - Main README:
README.md
Quick Reference: File → Responsibility
| File | What to Change |
|---|---|
| SDK integration, event translation |
| State management, orchestration, sessions |
| Add new event/action types |
| Extend plugin system |
| Layout, top-level UI structure |
| Conversation display with tool calls |
| Log display (optional, not in default layout) |
| Sidebar container and layout |
| Task tracking display |
| Context information display |
| Subagent monitoring |
| Git status display |
| Plan viewer |
| Model selection modal |
| Session management |
| Skills browser |
| Interactive questions |
| Theme configuration |
| Syntax highlighting |
| Prompt input with image support |
Troubleshooting
"Copilot not authenticated"
npm install -g @github/copilot-cli copilot auth login
Streaming not working
- Check
in session optionsstreaming: true - Verify adapter subscribes to
assistant.message_delta - Check harness emits
eventsassistant.delta
Cancellation not working
- SDK may not support true abort
- Current approach: ignore subsequent deltas, reset state
- Session remains valid for next prompt
Plugin not receiving events
- Verify plugin registered:
harness.use(plugin) - Check
method exists and has correct signatureonEvent - Add logging to verify events are being emitted
UI not updating
- Check component subscribes to harness events
- Verify state changes trigger re-render
- Use React DevTools to inspect component state
Future Enhancements (Planned)
Architecture supports these additions:
- Tasks: ✅ Implemented — Task tracking and status display
- Memory: Memory provider interface, context injection
- Git & Diffs: ✅ Partially implemented — Diff viewing, modified files; approval gating pending
- GitHub PR: PR tools, PR resources, PR pane
- File Browser: File selection pane, context injection
- Sessions: ✅ Implemented — Multi-session support with persistence
- Skills: ✅ Implemented — Skills browser and invocation
- Image Support: ✅ Implemented — Image attachment for vision models
- Plans: ✅ Implemented — Plan tracking and display
- Subagents: ✅ Implemented — Subagent monitoring and status
Summary
This skill provides comprehensive knowledge for working on the Copilot SDK TUI Harness project. Key takeaways:
- Strict separation: UI never imports SDK
- Event-driven: All communication via events/actions
- Plugin-ready: Extensibility built into core
- Three layers: UI → Harness → Adapter → SDK
- TypeScript + OpenTUI: Strongly typed, terminal-first
When developing, always consider: Which layer does this belong in? What events does it produce/consume? How does it fit into the plugin system?