Skip to content

Move PTY buffer out of zustand; drop input queueing#31

Merged
willwashburn merged 2 commits into
mainfrom
claude/decouple-pty-buffer
May 23, 2026
Merged

Move PTY buffer out of zustand; drop input queueing#31
willwashburn merged 2 commits into
mainfrom
claude/decouple-pty-buffer

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Follow-up to #29 / #30 for the "still feels sticky when typing fast" report. Skips local echo per your direction; does the other three.

What changes

Renderer — get the PTY buffer out of zustand.

  • New pty-buffer-store.ts: per-agent string[] buffer with key-scoped pub/sub, kept outside zustand entirely. Subscribers register against a single agent key.
  • agent-store: drops the ptyBuffer field from the Agent interface. worker_stream now appends to the pty-buffer-store and only touches zustand state when the agent's activity actually needs to flip (idle → active). During a steady typing session activity is already active/working, so the agents array isn't rewritten at all per chunk. agent_exited / agent_released clears the per-agent buffer.
  • use-terminal: subscribes by key (subscribePtyBuffer) instead of selecting from the agents array.
  • AgentNode: reads preview chunks via a tiny useEffect-based hook so the graph preview re-renders only for the agent producing chunks, not for every node.

Net effect: ProjectSidebar, TerminalPane, GraphView, etc. stop re-rendering on every PTY chunk.

Main — drop input queueing, fire-and-forget input.

  • Replace queueInput / scheduleInputFlush / flushQueuedInput / inputQueues with a single sendInputFireAndForget. We don't need backpressure for human keystrokes, and await stream.send(data) per byte was adding round-trip latency that shows up as stickiness when typing fast. Errors are logged; the existing stream-fallback path in sendInput still reopens broken streams.
  • ipc-handlers: route broker:send-input-fast to the new method.

Test plan

  • npm test — green.
  • npm run build — clean.
  • tsc -p tsconfig.web.json --noEmit and tsc -p tsconfig.node.json --noEmit — no errors in touched files.
  • Manual: hold down a key in a terminal pane and confirm characters paint without sticking. Verify that switching tabs while output is streaming still replays correctly (covers the snapshot/replay path that uses getAgentBuffer).
  • Manual: open the graph view while an agent is producing output and confirm the preview tile still updates.
  • Manual: release/exit an agent and confirm we don't leak entries in the pty-buffer-store (re-spawning the same name should not show stale output).

https://claude.ai/code/session_01BFwPP9s4vXkgsPDtBByLS9


Generated by Claude Code

Follow-up to #29 and #30 for the "sticky when typing fast" report.

The remaining cost on the hot path was that every PTY chunk rebuilt the
agents array in zustand, which woke every component reading
`useAgentStore((s) => s.agents)`. Even with the terminal's narrow
selector, components like ProjectSidebar, TerminalPane, and GraphView
were re-rendering 100+ times/sec for the echo of the user's own keys.

Renderer:
- New pty-buffer-store: per-agent string[] buffer with key-scoped
  pub/sub, sitting outside zustand. Terminal and AgentNode subscribe
  directly by agent key.
- agent-store: drop the `ptyBuffer` field from the Agent interface.
  worker_stream now appends to the pty-buffer-store and only touches
  zustand state when the agent's activity actually needs to flip from
  idle → active (which usually means: not at all during a typing
  session). agent_exited/agent_released clears the per-agent buffer.
- use-terminal: subscribe by key instead of selecting from the agents
  array.
- AgentNode: read preview chunks via a small useEffect-based hook that
  subscribes to the buffer for that node only.

Main:
- broker: replace queueInput / scheduleInputFlush / flushQueuedInput
  with a single fire-and-forget sendInput. We don't need backpressure
  for human typing, and the await on stream.send was adding round-trip
  latency per byte. Errors are logged; the existing stream-fallback
  path still reopens broken streams.
- ipc-handlers: route broker:send-input-fast to the new method.

https://claude.ai/code/session_01BFwPP9s4vXkgsPDtBByLS9
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 7e84ffee-2f2a-4006-b656-94f59e42e282

📥 Commits

Reviewing files that changed from the base of the PR and between b41cee8 and 3cdfc21.

📒 Files selected for processing (1)
  • src/renderer/src/stores/pty-buffer-store.ts

📝 Walkthrough

Walkthrough

Broker input switches from queued/coalesced delivery to a fire-and-forget sendInput API. A new per-agent PTY buffer store (with get/append/clear/subscribe) replaces in-state ptyBuffer, agent state is updated to use it, and terminal hooks/components consume PTY chunks via subscriptions.

Changes

Input and PTY Buffer Refactoring

Layer / File(s) Summary
Broker fire-and-forget input delivery
src/main/broker.ts, src/main/ipc-handlers.ts
The queued input mechanism is removed (QueuedInput, inputQueues, batching/flush timers). A new sendInputFireAndForget trims/validates and calls sendInput asynchronously (logs failures). IPC broker:send-input-fast now calls the new method; shutdown no longer clears queued timers.
New PTY buffer store module
src/renderer/src/stores/pty-buffer-store.ts
Adds an in-memory PTY buffer store keyed by agent key with getPtyChunks, appendPtyChunk (with trimming), clearPtyBuffer, and subscribePtyBuffer (returns unsubscribe). Notifies listeners on updates.
Agent store migration to external PTY buffer
src/renderer/src/stores/agent-store.ts
Removes ptyBuffer from the Agent interface and initialization paths. Routes worker_stream chunks to appendPtyChunk, clears buffers on agent exit, and makes getAgentBuffer return getPtyChunks(getAgentKey(...)). Agent activity/state updates remain separate.
Terminal hook and component refactoring
src/renderer/src/hooks/use-terminal.ts, src/renderer/src/components/graph/AgentNode.tsx
useTerminal subscribes to subscribePtyBuffer using getAgentKey and seeds from getPtyChunks; writeFromBuffer expects string[]. AgentNode adds useAgentPreviewChunks, computes preview from chunks, and no longer reads agent.ptyBuffer.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Inputs now hop without a queue,
Soft chunks of PTY hop anew,
Stores hum quietly, listeners sing,
Terminals wake from buffered spring,
A rabbit nudges code: “Hooray!” ✨


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

Copy link
Copy Markdown

@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: b41cee8cc6

ℹ️ 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 +25 to +26
existing.push(chunk)
next = existing
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit new PTY buffer arrays on every append

appendPtyChunk mutates the existing chunk array in place (existing.push) and notifies listeners with the same reference. Subscribers like AgentNode store these chunks in React state via setChunks(next), and React skips re-render when the reference is unchanged, so terminal preview tiles can stop updating after the first chunk (until a trim path creates a new array). Return a fresh array for each append before notifying listeners to keep keyed subscribers reactive.

Useful? React with 👍 / 👎.

In-place existing.push(chunk) handed listeners the same array reference,
so React subscribers (AgentNode's setChunks) bailed on Object.is and the
graph preview tile would freeze after the first chunk until a trim path
happened to allocate.

https://claude.ai/code/session_01BFwPP9s4vXkgsPDtBByLS9
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.

2 participants