# TUI Engine

**Source:** `src/tui/tui.ts` (939 lines)  
**Category:** UI/UX Components  
**Purpose:** Terminal-based chat interface with real-time gateway streaming

## Overview

The TUI Engine provides a full-featured terminal user interface for SophiaClaw, implementing chat interactions, session management, agent switching, and real-time event streaming from the gateway. Built on top of `@mariozechner/pi-tui`, it transforms a basic terminal into an interactive chat client with syntax highlighting, autocomplete, and overlay dialogs.

```
┌──────────────────────────────────────────────────────────────┐
│ Header: sophiaclaw tui - agent default - session main        │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  [Chat Log - scrollable message history]                    │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 👤 User: What's the weather?                           │ │
│  │ 🤖 Assistant: Let me check... [thinking: low]          │ │
│  │ 🔧 Tool: run_shell_command → "wttr.in"                 │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  [Status Line: waiting • 2s | connected]                    │
├──────────────────────────────────────────────────────────────┤
│ Footer: agent default | session main | anthropic/claude-sonnet | think low | 12k tokens │
├──────────────────────────────────────────────────────────────┤
│ [Editor Input: Type message... (Ctrl+C to exit)]             │
└──────────────────────────────────────────────────────────────┘
```

## Architecture

### Component Hierarchy

```
TUI (root)
├── Text (header) - Connection and session info
├── ChatLog - Scrollable message history
│   ├── UserMessage
│   ├── AssistantMessage
│   ├── SystemMessage
│   └── ToolCallDisplay
├── Container (status) - Dynamic status text/loader
├── Text (footer) - Model, tokens, settings
└── CustomEditor - Input field with history/autocomplete
```

### State Management

The TUI maintains mutable state through a `TuiStateAccess` object that tracks:

- **Session State:** `currentSessionKey`, `currentSessionId`, `sessionInfo`
- **Agent State:** `currentAgentId`, `agentDefaultId`, `agents[]`
- **Connection State:** `isConnected`, `connectionStatus`, `activityStatus`
- **Chat State:** `activeChatRunId`, `historyLoaded`, `toolsExpanded`
- **UI State:** `showThinking`, `autoMessageSent`, `pairingHintShown`

## Core Algorithms

### 1. Burst Coalescing for Paste Handling

**Problem:** Multiline pastes on Windows Git Bash and some macOS terminals arrive as rapid single-line submissions, causing fragmented messages.

**Algorithm:**

```typescript
function createSubmitBurstCoalescer(params): (value: string) => void {
  let pending: string | null = null;
  let pendingAt = 0;
  let flushTimer: Timeout | null = null;

  return (value: string) => {
    // Multiline content always flushes immediately
    if (value.includes("\n")) {
      flushPending();
      params.submit(value);
      return;
    }

    const ts = now();

    // First message in potential burst
    if (pending === null) {
      pending = value;
      pendingAt = ts;
      scheduleFlush(); // Debounce timer
      return;
    }

    // Within burst window - coalesce
    if (ts - pendingAt <= windowMs) {
      pending = `${pending}\n${value}`;
      pendingAt = ts;
      scheduleFlush();
      return;
    }

    // Outside window - flush old, start new
    flushPending();
    pending = value;
    pendingAt = ts;
    scheduleFlush();
  };
}
```

**Parameters:**

- `burstWindowMs`: 50ms default (tuned for paste speed)
- `enabled`: Platform detection via `shouldEnableWindowsGitBashPasteFallback()`

**Platform Detection:**

```typescript
function shouldEnableWindowsGitBashPasteFallback(): boolean {
  // macOS: iTerm2, Apple Terminal enabled
  if (platform === "darwin") {
    return termProgram.includes("iterm") || termProgram.includes("apple_terminal");
  }

  // Windows: MSYS2, MinGW, Mintty, Git Bash
  if (platform !== "win32") return false;

  return (
    msystem.startsWith("MINGW") ||
    msystem.startsWith("MSYS") ||
    shell.includes("bash") ||
    termProgram.includes("mintty")
  );
}
```

### 2. Backspace Deduplication

**Problem:** Some terminals emit multiple backspace keycodes for a single physical press.

**Algorithm:**

```typescript
function createBackspaceDeduper(dedupeWindowMs = 8ms): (data: string) => string {
  let lastBackspaceAt = -1

  return (data: string): string => {
    if (!matchesKey(data, Key.backspace)) {
      return data  // Pass through non-backspace
    }

    const ts = now()

    // Suppress duplicate within window
    if (lastBackspaceAt >= 0 && ts - lastBackspaceAt <= dedupeWindowMs) {
      return ''  // Consume duplicate
    }

    lastBackspaceAt = ts
    return data  // Pass first backspace
  }
}
```

### 3. Session Key Resolution

**Algorithm:** Resolves user input into normalized session keys supporting global, per-agent, and custom sessions.

```typescript
function resolveTuiSessionKey(params): string {
  const trimmed = params.raw.trim();

  // Empty input → use scope policy
  if (!trimmed) {
    if (params.sessionScope === "global") {
      return "global";
    }
    return buildAgentMainSessionKey({
      agentId: params.currentAgentId,
      mainKey: params.sessionMainKey,
    });
  }

  // Reserved keywords
  if (trimmed === "global" || trimmed === "unknown") {
    return trimmed;
  }

  // Explicit agent session
  if (trimmed.startsWith("agent:")) {
    return trimmed;
  }

  // Implicit agent session (prepend current agent)
  return `agent:${params.currentAgentId}:${trimmed}`;
}
```

**Session Key Format:**

- `global` - Shared across all agents
- `unknown` - Unidentified sender
- `agent:{agentId}` - Agent's main session
- `agent:{agentId}:{sessionId}` - Named session within agent

### 4. Ctrl+C Exit Sequence

**Algorithm:** Implements double-tap exit with input buffer protection.

```typescript
function resolveCtrlCAction(params): { action; nextLastCtrlCAt } {
  // First Ctrl+C: Clear input buffer if non-empty
  if (params.hasInput) {
    return { action: "clear", nextLastCtrlCAt: params.now };
  }

  // Second Ctrl+C within window: Exit
  if (params.now - params.lastCtrlCAt <= exitWindowMs) {
    return { action: "exit", nextLastCtrlCAt: params.lastCtrlCAt };
  }

  // First Ctrl+C on empty input: Warn
  return { action: "warn", nextLastCtrlCAt: params.now };
}
```

**Behavior:**

1. User has typed text → Clear buffer, show warning
2. User has no text, first tap → "press ctrl+c again to exit"
3. User taps again within 1000ms → Exit immediately

### 5. Dynamic Status Rendering

**Problem:** Different activity states require different visual feedback (static text vs animated loader).

**Algorithm:**

```typescript
const busyStates = new Set(["sending", "waiting", "streaming", "running"]);

function renderStatus() {
  const isBusy = busyStates.has(activityStatus);

  if (isBusy) {
    // Show animated spinner with dynamic message
    if (!statusStartedAt || lastActivityStatus !== activityStatus) {
      statusStartedAt = Date.now(); // Reset timer on state change
    }

    ensureStatusLoader(); // Swap text → spinner

    if (activityStatus === "waiting") {
      // Random encouraging phrase + tick animation
      startWaitingTimer(); // 120ms tick
    } else {
      // Standard elapsed time display
      startStatusTimer(); // 1s tick
    }

    updateBusyStatusMessage();
  } else {
    // Show static status text
    stopStatusTimer();
    stopWaitingTimer();
    statusLoader?.stop();
    ensureStatusText(); // Swap spinner → text
    statusText?.setText(theme.dim(`${connectionStatus} | ${activityStatus}`));
  }
}
```

**Waiting State Animation:**

```typescript
function buildWaitingStatusMessage(params): string {
  const phrase = params.phrases?.[tick % phrases.length] ?? "waiting";
  const dots = ".".repeat(tick % 4); // Animated dots
  return `${phrase}${dots} • ${elapsed} | ${connectionStatus}`;
}
```

### 6. Gateway Reconnection with Backoff

**Reconnection Strategy:** Exponential backoff with gap detection.

```typescript
private backoffMs = 1000  // Start at 1s

private scheduleReconnect() {
  if (this.closed) return

  const delay = this.backoffMs
  this.backoffMs = Math.min(this.backoffMs * 2, 30_000)  // Cap at 30s

  setTimeout(() => this.start(), delay).unref()
}

// Reset on successful connect
this.backoffMs = 1000
```

**Gap Detection:**

```typescript
if (seq !== null) {
  if (this.lastSeq !== null && seq > this.lastSeq + 1) {
    this.opts.onGap?.({
      expected: this.lastSeq + 1,
      received: seq,
    });
  }
  this.lastSeq = seq;
}
```

### 7. Tick Watchdog for Silent Stall Detection

**Problem:** Gateway may become unresponsive without closing connection.

**Algorithm:**

```typescript
// Gateway advertises tick interval in connect response (default 30s)
this.tickIntervalMs = helloOk.policy?.tickIntervalMs ?? 30_000;

// Start watchdog
function startTickWatch() {
  this.tickTimer = setInterval(() => {
    if (!this.lastTick) return;

    const gap = Date.now() - this.lastTick;
    if (gap > this.tickIntervalMs * 2) {
      this.ws?.close(4000, "tick timeout"); // Force reconnect
    }
  }, interval);
}

// Reset on each tick event
if (evt.event === "tick") {
  this.lastTick = Date.now();
}
```

### 8. Bash Command Execution (Bang Lines)

**Algorithm:** Detect and execute shell commands prefixed with `!`.

```typescript
function createEditorSubmitHandler(params) {
  return (text: string) => {
    const raw = text;
    const value = raw.trim();

    // Bash mode: must start with '!' and not be lone '!'
    if (raw.startsWith("!") && raw !== "!") {
      params.editor.addToHistory(raw);
      void params.handleBangLine(raw); // Execute shell command
      return;
    }

    // Slash commands
    if (value.startsWith("/")) {
      void params.handleCommand(value);
      return;
    }

    // Normal chat message
    void params.sendMessage(value);
  };
}
```

**Key Detail:** Uses raw (untrimmed) text to prevent leading spaces from triggering bash mode.

### 9. Local Run ID Tracking

**Purpose:** Identify messages initiated from this TUI instance vs remote/gateway-initiated messages.

```typescript
const localRunIds = new Set<string>(); // Max 200 entries

function noteLocalRunId(runId: string) {
  localRunIds.add(runId);
  if (localRunIds.size > 200) {
    const [first] = localRunIds;
    localRunIds.delete(first); // FIFO eviction
  }
}

function isLocalRunId(runId: string): boolean {
  return localRunIds.has(runId);
}
```

**Usage:** Prevents echo of locally-initiated messages when they arrive back from gateway.

## Keyboard Shortcuts

| Shortcut  | Action                          |
| --------- | ------------------------------- |
| `Ctrl+C`  | Clear input / Exit (double-tap) |
| `Ctrl+D`  | Exit immediately                |
| `Ctrl+O`  | Toggle tools expanded/collapsed |
| `Ctrl+L`  | Open model selector             |
| `Ctrl+G`  | Open agent selector             |
| `Ctrl+P`  | Open session selector           |
| `Ctrl+T`  | Toggle thinking visibility      |
| `Escape`  | Abort active streaming          |
| `Up/Down` | Navigate editor history         |

## Gateway Event Handling

```typescript
client.onEvent = (evt) => {
  if (evt.event === "chat") {
    handleChatEvent(evt.payload); // Messages, tool calls, status
  }
  if (evt.event === "agent") {
    handleAgentEvent(evt.payload); // Agent lifecycle changes
  }
};
```

**Chat Event Types:**

- `chat.message` - New message (user/assistant/system)
- `chat.tool_call` - Tool execution start
- `chat.tool_result` - Tool execution result
- `chat.status` - Streaming status updates
- `chat.session.patch` - Session metadata changes

## Connection Lifecycle

```
1. Create client + TUI instances
2. Register input listeners (backspace dedupe)
3. Build UI component tree
4. Start gateway WebSocket client
5. Wait for connection
6. On connect:
   - Send `connect` request with device credentials
   - Receive `hello.ok` with policy
   - Start tick watchdog
   - Load session history
   - Load agents list
7. Process events → update UI
8. On disconnect:
   - Show pairing hint if needed
   - Schedule reconnection with backoff
9. On exit:
   - Close WebSocket
   - Stop TUI rendering
   - Clear signal handlers
```

## Performance Optimizations

1. **Burst coalescing:** Prevents message fragmentation from paste
2. **Backspace dedupe:** Reduces input latency on problematic terminals
3. **Lazy history loading:** Only loads visible session history
4. **Local run ID set:** Bounded to 200 entries (FIFO eviction)
5. **Tick watchdog:** Detects silent stalls without polling
6. **Exponential backoff:** Prevents connection storms on gateway restart

## Security Considerations

1. **Input sanitization:** All user input treated as untrusted
2. **Gateway URL validation:** Blocks plaintext ws:// to non-loopback (CWE-319)
3. **Device credential storage:** Uses encrypted device-auth store
4. **TLS fingerprint verification:** Optional pinning for WSS connections
5. **Pairing enforcement:** Requires explicit approval for new devices

## Related Documentation

- [Gateway Protocol](/docs/reference/gateway-protocol.md)
- [Session Key Format](/docs/routing/session-key.md)
- [TUI Commands](/docs/cli/tui.md)
- [Device Pairing](/docs/gateway/device-pairing.md)
