Skip to content

feat: implement terminal streaming with diff algorithm#113

Open
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-75-terminal-stream
Open

feat: implement terminal streaming with diff algorithm#113
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-75-terminal-stream

Conversation

@2witstudios
Copy link
Owner

Summary

  • Per-agent terminal output streaming with longest-common-suffix diff algorithm that only sends new lines
  • Shared polling timer across subscribers watching the same agent (500ms interval, single capturePane call per tick)
  • Lazy initialization: polling starts on first subscriber, auto-cleans up when last unsubscribes
  • Error handling for dead/missing tmux panes with subscriber notification

Files

  • src/server/ws/terminal.tsTerminalStreamer class, diffLines() pure function, SendFn interface
  • src/server/ws/terminal.test.ts — 23 tests covering diff algorithm, subscription lifecycle, shared timer, polling, error handling, and cleanup

Test plan

  • diffLines — empty inputs, identical buffers, appended lines, scrolled buffer, complete rewrite, partial overlap
  • Subscription lifecycle — first subscriber starts polling, shared timer, partial unsub keeps timer, full unsub cleans up, independent agents
  • Polling & diff — initial content sends all, unchanged skips, appended sends only new, broadcast to all subscribers, interval timing
  • Error handling — dead pane sends error + cleans up, dead subscriber gets removed
  • Shared timer — single capture call per interval regardless of subscriber count
  • Destroy — tears down all streams and timers

Closes #75

Per-agent terminal output streaming with efficient longest-common-suffix
diff algorithm that only sends new lines, not the full buffer.

- Per-agent subscriptions with lazy initialization (polling starts on
  first subscriber, stops when last unsubscribes)
- 500ms polling interval for tmux pane content via capturePane()
- Longest common suffix diff — finds overlap between previous and current
  buffer snapshots to emit only new lines
- Shared timer across clients watching the same agent (single capture per
  interval regardless of subscriber count)
- Auto-cleanup when subscriber count drops to 0
- Error handling for dead/missing panes with subscriber notification
- Injectable capture function for testability
- 23 tests covering diff algorithm, subscription lifecycle, shared timer,
  polling behavior, error handling, and cleanup

Closes #75
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 7 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 34deb69 and 9c95be1.

📒 Files selected for processing (3)
  • src/commands/spawn.test.ts
  • src/server/ws/terminal.test.ts
  • src/server/ws/terminal.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/issue-75-terminal-stream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3794c3912c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 179 to 181
stream.timer = setInterval(() => {
void this.poll(agentId, stream);
}, this.pollIntervalMs);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize polling to prevent stale snapshot races

startPolling schedules poll via setInterval without waiting for the previous async poll to finish, so if capture takes longer than pollIntervalMs two polls can overlap; an older capture that resolves later can overwrite stream.lastLines with stale data, which then causes duplicated or missing lines in subsequent terminal messages. Guarding with an in-flight flag (or chaining with setTimeout after completion) avoids this ordering corruption.

Useful? React with 👍 / 👎.

Comment on lines 203 to 206
} catch {
// Dead client — remove on next tick
stream.subscribers.delete(sub.id);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tear down stream when last send throws

When sub.send throws, the subscriber is deleted, but there is no follow-up cleanup if that was the final subscriber, leaving an empty stream that continues polling tmux indefinitely. This creates unnecessary background load and keeps stale stream state alive until some later capture error happens; after the send loop, check for stream.subscribers.size === 0 and clear/delete the stream immediately.

Useful? React with 👍 / 👎.

- Switch from setInterval to chained setTimeout to prevent concurrent
  poll races when capturePane takes longer than the poll interval
- Replace loose equality (!=) with strict equality (===) in isPolling
- Remove unused loop variable in destroy()
- Log original error in catch block for debugging (console.error)
- Add test for double-unsubscribe idempotency
- Add test for trailing empty lines from tmux capturePane output
- Verify error logging in pane-failure test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement terminal streaming with diff algorithm

1 participant