Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ It does **not** analyze project code or prevent concurrent write conflicts insid
- **Timeout Handling**: Client implements 5s handshake timeout with SSE fallback.
- **Resize Support**: Client sends `{"type":"resize","cols":80,"rows":24}` to update PTY size, triggering TUI programs to redraw.
- **Dual Resize**: First resize starts data flow, second resize (50ms after first data) ensures complete TUI redraw.
- **Client-side Config**: Frontend uses xterm.js for terminal rendering. Terminal buffer size (`scrollback`) is configurable on the client side to control how much history is retained in memory for scrolling. This is a client-side setting and does not affect server-side log persistence.
- Fallback path remains available: HTTP input `POST /api/instances/input` + replay/SSE logs (`GET /api/instances/log`, `GET /api/instances/log/stream`).
- UI shows transport state (`websocket/sse/polling`) and supports manual WS reconnect.
- Backlog is stored on disk with a size cap (rolling truncate).
Expand Down Expand Up @@ -94,14 +95,19 @@ When switching between instances (worktree or instance tabs), the following sequ
Phase 1: Disconnect Previous
├── 1.1 Call disconnectTTY() - close old WebSocket
├── 1.2 Reset logCursor = 0
└── 1.3 Clear terminal buffers (if terminal exists)
├── For running instances:
│ ├── Write '\x1b[?1049l' (exit alternate buffer)
│ ├── Write '\x1b[2J\x1b[H' (clear normal buffer, home cursor)
│ └── Write '\x1b[?1049h' (re-enter alternate buffer, now clean)
└── For stopped instances:
├── Call term.reset()
└── Write '\x1b[2J\x1b[3J\x1b[H' (clear screen and scrollback)
└── 1.3 Reset terminal modes (if terminal exists)
│ ├── Write '\x1b[?1000l' (disable mouse button press/release)
│ ├── Write '\x1b[?1002l' (disable mouse drag)
│ ├── Write '\x1b[?1003l' (disable mouse all motion)
│ └── Write '\x1b[?1006l' (disable SGR extended mouse mode)
└── 1.4 Clear terminal buffers
├── For running instances:
│ ├── Write '\x1b[?1049l' (exit alternate buffer)
│ ├── Write '\x1b[2J\x1b[H' (clear normal buffer, home cursor)
│ └── Write '\x1b[?1049h' (re-enter alternate buffer, now clean)
└── For stopped instances:
├── Call term.reset()
└── Write '\x1b[2J\x1b[3J\x1b[H' (clear screen and scrollback)

Phase 2: Load Initial Data
├── 2.1 If stopped: loadLog() once, update status to "stopped", DONE
Expand All @@ -116,10 +122,13 @@ Phase 3: Establish New Connection (running instances only)
├── 3.3 Wait for server {"type": "ready"} message
├── 3.4 Set state to READY
├── 3.5 Send initial resize: {"type": "resize", "cols": N, "rows": M}
│ └── TUI programs receive SIGWINCH and re-initialize (including mouse modes)
└── 3.6 NOW and ONLY NOW: Call focusTerminalIfPossible()
```

**Critical Timing Rule:** The terminal MUST NOT receive focus until Phase 3.6 (after `ready` message is received and resize is sent).
**Critical Timing Rules:**
1. Terminal modes MUST be reset before switching to prevent mode leakage from previous TUI programs.
2. The terminal MUST NOT receive focus until Phase 3.6 (after `ready` message is received and resize is sent).

### 5.4 Focus Management Rules

Expand Down
110 changes: 102 additions & 8 deletions docs/TERMINAL_TEST_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,74 @@ These test the frontend `isTerminalQueryResponse()` function.

**Code**: `internal/ui/static/index.html:isTerminalQueryResponse()`

### 7. ESC Key Handling (Frontend)

These verify that ESC key and common ESC sequences pass through correctly.

| ID | Test Name | Input | Length | Expected |
|----|-----------|-------|--------|----------|
| E1 | Single ESC | `\x1b` | 1 | Preserved (length < MIN_RESPONSE_LENGTH) |
| E2 | ESC + bracket | `\x1b[` | 2 | Preserved (not a response pattern) |
| E3 | Arrow up | `\x1b[A` | 3 | Preserved (not `\[\d+;\d+R$` pattern) |
| E4 | Arrow in app mode | `\x1bOA` | 3 | Preserved (not OSC response) |
| E5 | Page Up | `\x1b[5~` | 4 | Preserved (not a response pattern) |
| E6 | F1 key | `\x1bOP` | 3 | Preserved (not `\x1b]` or `\x1bP` OSC) |

**Rationale**: User keyboard input including ESC sequences must pass through. Only xterm.js auto-generated responses (OSC color, Device Attributes, cursor position) are filtered. The MIN_RESPONSE_LENGTH=2 guard ensures single ESC passes.

**Code**: `internal/ui/static/index.html:isTerminalQueryResponse()`

### 8. Instance Switch Terminal Reset

These verify terminal state is properly reset when switching instances.

| ID | Test Name | Scenario | Expected Behavior |
|----|-----------|----------|-------------------|
| R1 | Mouse mode reset | Instance A has mouse tracking, switch to B | Mousemodesdisabled before switch |
| R2 | Alternate buffer reset | Instance A in alternate buffer, switch to B | Exit and re-enter alternate buffer |
| R3 | Stopped instance switch | Instance A stopped, switch to B | Full terminal reset |
| R4 | Running instance switch | Instance A running TUI, switch to running B | Buffer cleared, modes reset |
| R5 | Mode re-initialization | Switch to instance with TUI | TUI receives SIGWINCH, re-enables modes |

**Sequences sent on instance switch:**
```
\x1b[?1000l # Disable mouse button press/release
\x1b[?1002l # Disable mouse drag
\x1b[?1003l # Disable mouse all motion
\x1b[?1006l # Disable SGR extended mouse mode
```

**Code**: `internal/ui/static/index.html:selectInstance()`

### 9. UTF-8 Multi-byte Handling (Frontend)

These verify that UTF-8 multi-byte characters are correctly decoded across WebSocket frames.

| ID | Test Name | Input | Expected |
|----|-----------|-------|----------|
| U1 | Chinese split across frames | Frame1: `\xe4\xb8`, Frame2: `\xad` | `中` (U+4E2D) correctly decoded |
| U2 | Emoji split across frames | Frame1: `\xf0\x9f`, Frame2: `\x98\x80` | `😀` correctly decoded |
| U3 | Single frame Chinese | `\xe4\xb8\xad` | `中` correctly decoded |
| U4 | Mixed ASCII and Chinese | `hello中文world` | Correctly decoded and filtered |

**Rationale**: TextDecoder with`{stream: true}` handles incomplete multi-byte sequences across frames.

**Code**: `internal/ui/static/index.html:decodeTTYOutputChunk()`

## Test Statistics

| Category | Test Cases | Status |
|----------|-----------|--------|
| Secret redaction | 3 | ✓ Pass |
| Mouse residues | 4 | ⚠️Disabled |
| Mouse residues | 4 | ⚠️ Disabled |
| False positives | 4 | ✓ Pass |
| Legitimate sequences | 5 | ✓ Pass |
| Input handling | 4 | ✓ Pass |
| Query responses | 7 | ✓ Pass |
| **Total** | **27** | **87% Active** |
| ESC key handling | 6 | ✓ Pass |
| Instance switch reset | 5 | ✓ Pass |
| UTF-8 multi-byte | 4 | ✓ Pass |
| **Total** | **42** | **90% Active** |

## Adding New Test Cases

Expand All @@ -138,14 +195,51 @@ go run ./cmd/myworktree -listen 127.0.0.1:8080

# Open web UI
open http://localhost:8080

# Test scenarios:
# 1. Create opencode instance - verify TUI displays correctly
# 2. Exit opencode - return to shell
# 3. Switch between instances - verify no anomalous strings
# 4. Run vim/htop - verify display works
```

### Basic TUI Tests

1. Create opencode instance - verify TUI displays correctly
2. Exit opencode - return to shell, no anomalous strings
3. Run vim/htop - verify display works
4. Type Chinese characters - verify UTF-8 handling

### Instance Switch Tests

1. **Mouse tracking reset**:
- Create Instance A: run `opencode`
- Enable mouse tracking in opencode (mouse should work)
- Create Instance B: run `bash`
- Switch back to Instance A, then to Instance B
- Move mouse over terminal - no mouse event strings should appear

2. **TUI program switch**:
- Create Instance A: run `htop` (TUI with mouse)
- Create Instance B: run `vim` (TUI with alternate buffer)
- Switch between A and B multiple times
- Each TUI should display correctly without artifacts

3. **Chinese output in TUI**:
- Create Instance A: run `opencode`
- Have opencode output Chinese text
- Switch to Instance B, then back to Instance A
- Chinese characters should display correctly (no replacement characters)

4. **ESC key handling**:
- Create Instance A: run `vim`
- Press ESC key - should work as expected (exit insert mode)
- Press sequences like ESC+[ (should not be filtered)

### Expected Behavior on Switch

When switching from Instance A (with TUI) to Instance B:
1. Terminal sends mouse disable sequences
2. Terminal exits/re-enters alternate buffer
3. WebSocket disconnects from A
4. WebSocket connects to B
5. Resize sent to B → TUI in B receives SIGWINCH
6. TUI in B re-initializes (re-enables mouse if needed)

## Related Documents

- `TERMINAL_IO_ANALYSIS.md` - Architecture and analysis
Expand Down
7 changes: 7 additions & 0 deletions internal/ui/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,13 @@ <h3>Start Instance</h3>
const isRunning = inst && inst.status === 'running';

if (term) {
// Reset terminal modes before switching to prevent mode leakage
// from previous instance's TUI program (e.g., opencode with mouse tracking)
term.write('\x1b[?1000l'); // Disable mouse button press/release
term.write('\x1b[?1002l'); // Disable mouse drag
term.write('\x1b[?1003l'); // Disable mouse all motion
term.write('\x1b[?1006l'); // Disable SGR extended mouse mode

if (isRunning) {
// For running instances with TUI programs:
// Exit alternate buffer, clear normal buffer, re-enter alternate buffer
Expand Down