Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ npm test # Run tests
./cli/bin/mew.js space up
```

## Quick Testing Guide
```bash
# Create test space (from repo root)
cd tests && mkdir test-feature && cd test-feature
../../cli/bin/mew.js space init --template coder-agent .
cd ../.. && npm install # Link local packages

# Start space
cd tests/test-feature
../../cli/bin/mew.js space up # Default port 8080

# Send messages via HTTP API (replace test-feature with your folder name)
curl -X POST 'http://localhost:8080/participants/human/messages?space=test-feature' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer human-token' \
-d '{"type": "chat", "content": "Hello"}'

# View logs (non-blocking for coding agents)
pm2 logs --nostream --lines 50 # Last 50 lines, exits immediately
pm2 logs gateway --nostream # Gateway logs only, no streaming
pm2 logs coder --nostream --err # Agent errors only
tail -n 100 ~/.pm2/logs/gateway-out.log # Direct file access

# Check responses in curl (add -v for verbose)
curl -v -X POST ... # Shows request/response headers

# Interactive mode (better for development)
../../cli/bin/mew.js space connect
# Paste JSON directly to send protocol messages
# Type normally for chat messages

# Clean up
../../cli/bin/mew.js space down
```

## Development Workflow
1. Read existing patterns before implementing
2. Update types first, then implementations
Expand All @@ -58,6 +93,7 @@ npm test # Run tests
- DO follow TypeScript strict mode
- DO run relevant tests after changes
- DO update specs if changing protocol
- DO send stream/close when done with a stream (agents must clean up)

## Making Changes
1. **Protocol changes**: Update spec/draft → types → implementations → tests
Expand Down
36 changes: 36 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ npm test # Run tests
./cli/bin/mew.js space up
```

## Quick Testing Guide
```bash
# Create test space (from repo root)
cd tests && mkdir test-feature && cd test-feature
../../cli/bin/mew.js space init --template coder-agent .
cd ../.. && npm install # Link local packages

# Start space
cd tests/test-feature
../../cli/bin/mew.js space up # Default port 8080

# Send messages via HTTP API (replace test-feature with your folder name)
curl -X POST 'http://localhost:8080/participants/human/messages?space=test-feature' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer human-token' \
-d '{"type": "chat", "content": "Hello"}'

# View logs (non-blocking for coding agents)
pm2 logs --nostream --lines 50 # Last 50 lines, exits immediately
pm2 logs gateway --nostream # Gateway logs only, no streaming
pm2 logs coder --nostream --err # Agent errors only
tail -n 100 ~/.pm2/logs/gateway-out.log # Direct file access

# Check responses in curl (add -v for verbose)
curl -v -X POST ... # Shows request/response headers

# Interactive mode (better for development)
../../cli/bin/mew.js space connect
# Paste JSON directly to send protocol messages
# Type normally for chat messages

# Clean up
../../cli/bin/mew.js space down
```

## Development Workflow
1. Read existing patterns before implementing
2. Update types first, then implementations
Expand All @@ -58,6 +93,7 @@ npm test # Run tests
- DO follow TypeScript strict mode
- DO run relevant tests after changes
- DO update specs if changing protocol
- DO send stream/close when done with a stream (agents must clean up)

## Making Changes
1. **Protocol changes**: Update spec/draft → types → implementations → tests
Expand Down
90 changes: 88 additions & 2 deletions cli/spec/draft/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,8 @@ The default interactive mode uses Ink (React for CLI) to provide a modern termin
- Chat acknowledgement and cancellation shortcuts (`/ack`, `/cancel`) to manage UI queues
- Participant control shortcuts (`/status`, `/pause`, `/resume`, `/forget`, `/clear`, `/restart`, `/shutdown`)
- Stream negotiation helpers (`/stream request`, `/stream close`, `/streams`) with live frame previews
- Reasoning status card with `/reason-cancel` hint and automatic stream mirroring for thoughts
- Reasoning bar with token streaming, action display, and `/reason-cancel` control
- Fixed-height UI components to prevent jitter during real-time updates

**Architecture:**
- Message history uses Ink's `Static` component for native scrolling
Expand All @@ -1330,6 +1331,90 @@ The default interactive mode uses Ink (React for CLI) to provide a modern termin
- **Participant Status**: Displays most recent `participant/status` payloads (tokens, context occupancy, latency) per sender
- **Pause State**: Highlights active pauses applied to the CLI participant with time remaining indicators
- **Stream Monitor**: Lists current `stream/open` sessions and the latest raw frames decoded from `#streamId#` WebSocket traffic
- **Collapsed Summary**: When the board is hidden, the status bar surfaces a compact `acks`, `status`, and `paused` summary so operators can monitor high-level state without losing transcript space. The board remains collapsed unless toggled with `/ui board open`; `/ui board close` re-collapses it and `/ui board auto` re-enables auto-collapse when no activity is present.

#### Reasoning Bar Display

The advanced interactive mode includes a dedicated reasoning bar that appears when agents engage in reasoning sessions. This component provides real-time feedback about the agent's thought process, even when the underlying model doesn't stream the actual reasoning content.

**Visual Design:**
- Cyan rounded border distinguishes it from regular messages
- Fixed height (8 lines) to prevent UI jitter during streaming
- Animated spinner indicates active processing
- Horizontal layout spreads information across the width

**Information Display:**
```
╭─────────────────────────────────────────────────────────────╮
│ ⠋ mew is thinking 90s 32 tokens │
│ Using tool mcp-fs-bridge /read_text_file │
│ ...preview of reasoning text (max 3 lines)... │
│ │
│ Use /reason-cancel to interrupt reasoning. │
╰─────────────────────────────────────────────────────────────╯
```

**Content Elements:**
1. **Status Line**:
- Animated spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) for visual activity
- Participant name ("mew is thinking")
- Elapsed time in seconds
- Token count or thought count depending on available data

2. **Action Line** (when available):
- Shows current tool or action being executed
- Extracted from `reasoning/thought` payload's `action` field
- Example: "Using tool mcp-fs-bridge /read_text_file"

3. **Text Preview**:
- Limited to 3 lines and 400 characters to prevent height changes
- Shows latest reasoning text or message
- Italicized gray text for visual distinction
- Ellipsis prefix when truncated

4. **Control Hint**:
- Always displayed at bottom: "Use /reason-cancel to interrupt reasoning."
- Provides user with clear action to stop long-running reasoning

**Streaming and Token Counting:**

The reasoning bar handles two types of progress indication:

1. **Token Streaming** (OpenAI-style models):
- Receives token count updates via `stream/data` frames
- Displays running total as "X tokens"
- Updates in real-time as tokens are generated
- No actual reasoning content streamed (model limitation)

2. **Thought Counting** (legacy/fallback):
- Counts number of `reasoning/thought` messages
- Displays as "X thoughts"
- Used when token information not available

**Implementation Details:**

- **Fixed Layout**: All elements use fixed heights to prevent layout shifts:
- Main container: `height: 8`
- Action line: `height: 1`
- Text preview: `height: 3` (matches `maxLines`)
- Always renders elements with space character fallback to avoid Ink rendering errors

- **State Management**:
- Tracks `activeReasoning` in UI state
- Updates on `reasoning/start` messages
- Clears on `reasoning/conclusion` or `/reason-cancel`
- Merges token metrics from multiple sources (thoughts and streams)

- **Stream Integration**:
- Automatically subscribes to reasoning streams via `stream/request`
- Processes `stream/open` responses to track stream IDs
- Parses `#streamId#data` frames for token updates
- Handles `stream/close` for cleanup

**User Interactions:**
- `/reason-cancel [reason]` - Sends cancellation request to stop reasoning
- Escape key can be used in some contexts to cancel
- Visual feedback shows cancellation was sent

### Debug Mode (--debug flag)

Expand Down Expand Up @@ -1519,6 +1604,7 @@ Verbose mode (`/verbose`) shows full JSON messages.
/help Show available commands and shortcuts
/verbose Toggle verbose output (show full JSON)
/ui-clear Clear local UI buffers (history, status board)
/ui board [open|close|auto] Control Signal Board docking behaviour
/exit Disconnect and exit
/ack [selector] [status] Acknowledge pending chat message(s)
/cancel [selector] [reason] Cancel pending chat message(s)
Expand Down Expand Up @@ -1628,4 +1714,4 @@ This CLI successfully implements the test plan when:
2. Gateway starts and accepts connections
3. Agents respond appropriately
4. FIFO mode enables test automation
5. Messages flow correctly between participants
5. Messages flow correctly between participants
96 changes: 93 additions & 3 deletions cli/src/commands/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ gateway
const wss = new WebSocket.Server({ server });

// Track spaces and participants
const spaces = new Map(); // spaceId -> { participants: Map(participantId -> ws) }
const spaces = new Map(); // spaceId -> { participants: Map(participantId -> ws), streamCounter: number, activeStreams: Map }

// Track participant info
const participantTokens = new Map(); // participantId -> token
Expand Down Expand Up @@ -563,7 +563,34 @@ gateway

ws.on('message', async (data) => {
try {
const message = JSON.parse(data.toString());
const dataStr = data.toString();

// Check if this is a stream data frame (format: #streamID#data)
if (dataStr.startsWith('#') && dataStr.indexOf('#', 1) > 0) {
const secondHash = dataStr.indexOf('#', 1);
const streamId = dataStr.substring(1, secondHash);

// Forward stream data to all participants in the space
if (spaceId && spaces.has(spaceId)) {
const space = spaces.get(spaceId);

// Verify stream exists and belongs to this participant
const streamInfo = space.activeStreams.get(streamId);
if (streamInfo && streamInfo.participantId === participantId) {
// Forward to all participants
for (const [pid, pws] of space.participants.entries()) {
if (pws.readyState === WebSocket.OPEN) {
pws.send(data); // Send raw data frame
}
}
} else {
console.log(`[GATEWAY WARNING] Invalid stream ID ${streamId} from ${participantId}`);
}
}
return; // Don't process as JSON message
}

const message = JSON.parse(dataStr);

// Validate message
const validationError = validateMessage(message);
Expand Down Expand Up @@ -593,7 +620,11 @@ gateway

// Create space if it doesn't exist
if (!spaces.has(spaceId)) {
spaces.set(spaceId, { participants: new Map() });
spaces.set(spaceId, {
participants: new Map(),
streamCounter: 0,
activeStreams: new Map()
});
}

// Add participant to space
Expand Down Expand Up @@ -942,6 +973,65 @@ gateway
envelope.correlation_id = [envelope.correlation_id];
}

// Handle stream/request - gateway must respond with stream/open
if (envelope.kind === 'stream/request' && spaceId && spaces.has(spaceId)) {
const space = spaces.get(spaceId);

// Generate unique stream ID
space.streamCounter++;
const streamId = `stream-${space.streamCounter}`;

// Track the stream
space.activeStreams.set(streamId, {
requestId: envelope.id,
participantId: participantId,
direction: envelope.payload?.direction || 'unknown',
created: new Date().toISOString()
});

// Send stream/open response
const streamOpenResponse = {
protocol: 'mew/v0.3',
id: `stream-open-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
ts: new Date().toISOString(),
from: 'gateway',
to: [participantId],
kind: 'stream/open',
correlation_id: [envelope.id],
payload: {
stream_id: streamId,
encoding: 'text'
}
};

// Send stream/open to ALL participants (MEW Protocol visibility)
for (const [pid, pws] of space.participants.entries()) {
if (pws.readyState === WebSocket.OPEN) {
pws.send(JSON.stringify(streamOpenResponse));
}
}

if (options.logLevel === 'debug') {
console.log(`[GATEWAY DEBUG] Assigned stream ID ${streamId} for request from ${participantId}`);
}
}

// Handle stream/close - clean up active streams
if (envelope.kind === 'stream/close' && spaceId && spaces.has(spaceId)) {
const space = spaces.get(spaceId);

// Find and remove the stream (may be referenced by correlation_id)
if (envelope.payload?.stream_id) {
const streamId = envelope.payload.stream_id;
if (space.activeStreams.has(streamId)) {
space.activeStreams.delete(streamId);
if (options.logLevel === 'debug') {
console.log(`[GATEWAY DEBUG] Closed stream ${streamId}`);
}
}
}
}

// ALWAYS broadcast to ALL participants - MEW Protocol requires all messages visible to all
if (spaceId && spaces.has(spaceId)) {
const space = spaces.get(spaceId);
Expand Down
Loading
Loading