Dev.to AI 🤖 Ai 👁 0 📖 6 min read

Keeping a coding-agent client fast when sessions get huge

AI coding agents produce a lot of history. A single session can run for days, call hundreds of tools, read whole files, dump megabytes of test output and spawn subagents with histories of their own. Multiply that by a do

Keeping a coding-agent client fast when sessions get huge

CodeNomad on desktop and mobile

AI coding agents produce a lot of history. A single session can run for days, call hundreds of tools, read whole files, dump megabytes of test output and spawn subagents with histories of their own. Multiply that by a dozen projects open in parallel and the client sitting in front of the agent becomes the bottleneck.

We hit that wall while building CodeNomad, an open-source desktop and mobile client for OpenCode. Our users are people who keep many sessions and projects running at once and let agents work autonomously for long stretches. Here is what kept the interface responsive for them, with the numbers we measured along the way.

The short version: never load what you don't show, bound every render, query the data where it lives, and don't let background work starve the foreground.

1. Search the history without downloading it

The naive way to search a conversation is to load it into the client and scan it. With long agent sessions that means pulling every tool output into memory just to look for a word.

Instead, search runs where the history already lives. CodeNomad ships a small plugin that runs inside the OpenCode service and exposes an authenticated RPC over the stored history. The plugin binds a read-only SQLite connection to the daemon's own storage and scans in bounded pages:

  • a page examines at most 1,024 rows and stops at 32 matching messages;
  • the scan yields to the event loop between rows and ends the page after roughly 40 ms of work;
  • each message's JSON is materialized one at a time, capped at 16 MiB, and oversized content is reported as skipped instead of silently dropped.

There is deliberately no full-text index, no trigger and no second copy of the transcript. The UI keeps one bounded page of results (at most 63 hits) and asks for the next page on demand. Searching a whole workspace, every session in every worktree, never pages the transcript into the renderer.

Workspace-wide history search in CodeNomad

2. A timeline that never touches message bodies

Long sessions need navigation: a rail of markers you can scrub through and jump from. Drawing it from loaded messages would defeat the point.

The same plugin serves an outline: structural metadata only (IDs, sequence numbers, part types, technical-part counts, the first tool name for an icon), up to 16,384 entries per page, with no excerpts or bodies. SQL projects that metadata directly, so large content never enters JavaScript just to draw the rail.

Refreshing it is incremental. Rows are grouped into checkpoints of 512 row headers, and the client sends digests of the checkpoints it already has. Only changed ranges are recomputed, so reopening a big session revalidates instead of rescanning.

Jumping to a marker loads a bounded window around the target: up to 200 messages, 80 before and 120 from the target, with neighbor windows overlapping by 16 so the reading position survives crossing a boundary. The old transcript stays on screen until the destination is ready.

3. Bound every render

Agents produce text that is fine to store and terrible to render all at once. A 1 MB shell log with syntax highlighting will freeze any UI thread.

So every block renders a bounded prefix with an in-place "show more", and the full text stays in the store for copy and search:

Block Initial bound Why
Prose Markdown 50,000 characters about 35 ms to parse and lay out in Chromium
Tool output, diffs, errors 10,000 characters about 70 ms with syntax highlighting
Streaming reasoning 10,000 characters re-rendered on every delta

A few rules make the bounds invisible in practice. A window ends on a nearby line break, so code lines and terminal escape sequences stay whole. Revealing a huge remainder happens in steps of about 250,000 characters, so no single render jumps by more than a quarter megabyte. Markdown beyond 100,000 characters skips syntax highlighting. And while a reply streams, large windows re-render at most every 200 ms, without highlighting, then render once more, highlighted, when the reply completes.

4. Stop doing work nobody will see

Profiling showed that a surprising share of the cost was work whose result was thrown away:

  • Superseded Markdown. A burst of streaming updates triggered a parse for each intermediate version. Discarding superseded invocations before parsing took a cold burst from 33 parses to one.
  • Unchanged projections. The timeline rebuilt its full projection whenever any sub-bucket changed. Reusing it when the outline snapshot, marker identities and labels are unchanged took a 10,000-entry fixture from 64 full projections to zero.
  • Event replay. After a reconnect, queued events re-hydrated the complete page once per event. Making replay cooperative and publishing once per bounded chunk cut the longest task from 3,195 ms to 62 ms, and replay completion from 3,237 ms to 184 ms.

5. Highlight search results without touching the DOM

Highlighting matches used to rewrite the transcript's DOM, wrapping every hit in a <mark>. Besides the cost, it destroyed the user's text selection.

We switched to the CSS Custom Highlight API: the matches become Range objects registered with the browser, which paints them without changing the document. On a mixed transcript, synchronous work dropped from 16.45 ms to 4.05 ms (median, Chromium) and search DOM mutations from 3,708 to zero, and the selection now survives. It is not a universal win: in one Electron measurement the 95th-percentile paint latency got worse even though the median improved, which is a good reminder to look at more than one number.

6. Startup: the conversation you were reading comes first

On a slow Windows launch, the selected conversation appeared after 12.5 seconds. Two causes stood out in the captures:

  • The transcript read restarted three times. A readiness check inside a reactive effect re-ran the effect every time another restored workspace updated, cancelling the in-flight read and requeuing it behind other traffic. Reading that check outside the reactive scope fixed it.
  • Secondary reads held the browser's few connections to the server. Plugin controls, update checks and timeline outlines were in flight while the transcript waited almost two seconds for a connection. They now share a bounded background-read budget, so the foreground read always gets through.

The same investigation found virtualized rows refetching permission receipts on every mount: about 700 requests in four minutes on one session, the same message up to 12 times. A small cache that keeps each row's completed snapshot and shares in-flight reads removed the flood.

7. Background work can starve everything

One last example, because it surprised us. Users with many Git worktrees saw recovery warnings that looked like the OpenCode service was slow. It wasn't: the native read answered in about 5 ms. Our own ownership checks spawned git rev-parse --git-common-dir for every candidate directory, and on Windows each spawn costs 50 to 800 ms. With 314 worktrees re-checked every 30 seconds, the process queue never drained.

Resolving the common directory by reading Git's administrative files directly (.git, commondir, HEAD) removed the spawns for the normal cases, while unusual layouts still defer to Git itself.

Takeaways

  • Push queries to the data. A plugin next to the storage beats shipping history to the client.
  • Bound everything by default, and make the bound a reveal, not a truncation.
  • Discard superseded work early, before parsing, not after.
  • Budget background work so it can never block what the user is looking at.
  • Measure on real machines. Our worst problems only showed up on specific Windows installs, never in a clean fixture.

CodeNomad is MIT-licensed and works with OpenCode V2 on Windows, macOS, Linux and from a phone. If you run many agent sessions at once, I'd love to hear where it still feels slow: github.com/NeuralNomadsAI/CodeNomad.

Disclosure: I'm one of the CodeNomad maintainers.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.