Zulip DMs were fully buffered — the user saw nothing until the entire LLM response was complete. No way to steer the conversation mid-response, unlike the terminal TUI where you can Ctrl+C and type a follow-up.
Solution
Three-part upgrade to the session-injection architecture:
1. Streaming visibility
On DM receipt, sends a 🤖 _Thinking..._ placeholder message to Zulip immediately
message_update event handler accumulates partial text and edits the Zulip message every ~2 seconds with the live content
On agent_end, the placeholder is edited one final time with the complete response
2. Steering (mid-response interruption)
turn_start/agent_end lifecycle tracks isAgentBusy state
When a new Zulip DM arrives while the LLM is streaming, pi.sendUserMessage() is called with { deliverAs: "steer" } — this queues the message and delivers it after the current turn finishes
Effectively the same as typing a follow-up in the terminal: the original turn completes, the steer message becomes the next input, and a new turn starts
3. API additions
sendMessage() now returns the Zulip message ID
Added editMessage(id, content) using PATCH /messages/{id}
Streaming timer properly cleaned up on shutdown
Usage
Send a Zulip DM to the bot → see "Thinking..." immediately → watch the response stream in live → send another DM mid-stream to interrupt and steer.
## Problem
Zulip DMs were fully buffered — the user saw nothing until the entire LLM response was complete. No way to steer the conversation mid-response, unlike the terminal TUI where you can Ctrl+C and type a follow-up.
## Solution
Three-part upgrade to the session-injection architecture:
### 1. Streaming visibility
- On DM receipt, sends a `🤖 _Thinking..._` placeholder message to Zulip immediately
- `message_update` event handler accumulates partial text and edits the Zulip message every ~2 seconds with the live content
- On `agent_end`, the placeholder is edited one final time with the complete response
### 2. Steering (mid-response interruption)
- `turn_start`/`agent_end` lifecycle tracks `isAgentBusy` state
- When a new Zulip DM arrives while the LLM is streaming, `pi.sendUserMessage()` is called with `{ deliverAs: "steer" }` — this queues the message and delivers it after the current turn finishes
- Effectively the same as typing a follow-up in the terminal: the original turn completes, the steer message becomes the next input, and a new turn starts
### 3. API additions
- `sendMessage()` now returns the Zulip message ID
- Added `editMessage(id, content)` using `PATCH /messages/{id}`
- Streaming timer properly cleaned up on shutdown
## Usage
Send a Zulip DM to the bot → see "Thinking..." immediately → watch the response stream in live → send another DM mid-stream to interrupt and steer.
- Adds message_update handler that periodically edits the Zulip
placeholder with partial response text (every ~2s during streaming)
- Sends a 'Thinking...' placeholder to Zulip immediately on DM receipt
- Detects agent busy state and uses deliverAs: 'steer' so subsequent
Zulip DMs steer the conversation mid-stream (like terminal TUI)
- Adds editMessage() to the queue helper (Zulip PATCH /messages/{id})
- sendMessage() now returns the message ID for editing
- Turn_start / agent_end lifecycle tracks agent busy state
- Cleanup: streaming timer properly cleared on shutdown
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Zulip DMs were fully buffered — the user saw nothing until the entire LLM response was complete. No way to steer the conversation mid-response, unlike the terminal TUI where you can Ctrl+C and type a follow-up.
Solution
Three-part upgrade to the session-injection architecture:
1. Streaming visibility
🤖 _Thinking..._placeholder message to Zulip immediatelymessage_updateevent handler accumulates partial text and edits the Zulip message every ~2 seconds with the live contentagent_end, the placeholder is edited one final time with the complete response2. Steering (mid-response interruption)
turn_start/agent_endlifecycle tracksisAgentBusystatepi.sendUserMessage()is called with{ deliverAs: "steer" }— this queues the message and delivers it after the current turn finishes3. API additions
sendMessage()now returns the Zulip message IDeditMessage(id, content)usingPATCH /messages/{id}Usage
Send a Zulip DM to the bot → see "Thinking..." immediately → watch the response stream in live → send another DM mid-stream to interrupt and steer.
- Adds message_update handler that periodically edits the Zulip placeholder with partial response text (every ~2s during streaming) - Sends a 'Thinking...' placeholder to Zulip immediately on DM receipt - Detects agent busy state and uses deliverAs: 'steer' so subsequent Zulip DMs steer the conversation mid-stream (like terminal TUI) - Adds editMessage() to the queue helper (Zulip PATCH /messages/{id}) - sendMessage() now returns the message ID for editing - Turn_start / agent_end lifecycle tracks agent busy state - Cleanup: streaming timer properly cleared on shutdown