Version line
v2 — Go rewrite (1.x), main-v2 (active development)
What problem does this solve?
Reasonix has two frontend runtimes — a TUI (Bubbletea) and a Desktop app (Wails + React) — that share a common control.Controller backend. Over time, profiling revealed three categories of performance bottlenecks:
- Rendering frame rate (TUI) — The transcript view re-wraps the entire conversation on every token delta; lipgloss styles and goldmark renderers are recreated every frame; the status bar is built twice per frame (once in
Update counting lines, once in View displaying them).
- IPC frequency (Desktop) — Every streaming token delta is emitted as a separate Wails bridge call; the Go side allocates a new string per event via
Emit; no coalescing or backpressure exists; the React side rebuilds component trees on every update without useDeferredValue.
- Allocation / GC pressure (cross-layer) — SSE stream parsing copies every line from
[]byte to string; tool call arguments use += for delta accumulation (O(n²)); shell command output accumulates via string concatenation; JSON parsing for the todo panel fires every frame regardless of whether the data changed; project files are re-read from disk on every access.
These issues are most visible during long streaming conversations (>10k tokens), repeated tool call execution, and large shell command outputs — all common in daily use.
Proposed solution
Deliver three batches of zero-risk, independently verifiable optimisations across all four layers:
Batch 1 — Immediate gains (low risk):
- Replace SSE
string operations with zero-copy []byte (scanner.Bytes() + bytes.HasPrefix/bytes.Equal/bytes.TrimSpace)
- Cache lipgloss
Style objects at the package level, rebuilt only when width or colour changes (wrap, theme, queue indicator styles)
- Cache goldmark
mdRenderer instances per terminal width via sync.Map
- Replace
+= string concatenation with strings.Builder in connectorBlock, MCP view width calculations, and shell output accumulation
- Incremental
flushableMarkdownPrefix using a lastParagraphBreak byte offset
- File caching (
loadProjectsFile with a 5-second TTL and sync.RWMutex)
- Single-pass
streamedRows rune scanning instead of strings.Split + regex
- Replace
map[event.Kind]string with a [KindCount]string array in the wire layer
- Extract
event.Coalesce() as a shared exported function for continuous deltas
Batch 2 — Core optimisations (medium risk):
- Incremental transcript wrapping (
wrapDirtyFrom + lineWrapCounts), only re-wrapping the dirty tail instead of the entire transcript
- Eliminate double status-bar construction via
buildStatusLines caching (one call in Update, zero in View)
- Reuse package-level buffers (
renderRowsBuf/renderBarBuf/blankRow) in renderTranscript, plus strings.Builder output assembly
- Desktop Go event coalescing: buffer consecutive
Text/Reasoning deltas, flush on non-coalescable events, async emitter with ring-buffer backpressure (drop oldest 1/4 at queue capacity 1024)
- Desktop React:
memo on Transcript/StatusBar/UserMessage components, useCallback stabilisation for handleToggleColdPage/handleToggleWarmTurn, 77 useCallback wrappers in App.tsx
- Wire type deduplication (remove
desktop/wire.go, share eventwire.ToWire)
scrollVersion keyed on stable identifiers (id + kind + status) only, ignoring volatile text.length
return s guards in applyEvent reducer to return the same reference when nothing changed
Batch 3 — Dependent / cross-cutting optimisations:
- Tool call argument accumulation via
argsBuilders map[int]*strings.Builder in both OpenAI and Anthropic SSE handlers
- Rewrite
coalesceEventDeltas with a single strings.Builder per coalesced run, eliminating O(n²) string copy during long flush intervals
- Cache JSON parsing in the todo panel (
todoParsedArgs/todoParsedTodos, invalidated via setTodoArgs/refreshTodoCache)
- Cache bottom-panel row count (
buildBottomPanelsRowCount() called once in Update instead of rendering twice)
useDeferredValue(text) in MarkdownRenderer to defer expensive markdown rendering during streaming; extract REMARK_PLUGINS/REHYPE_PLUGINS as module-level constants
All optimisations preserve the control.Controller transport-agnostic contract, maintain existing test coverage, and are independently verifiable (go vet + go test + npx tsc).
Version line
v2 — Go rewrite (1.x), main-v2 (active development)
What problem does this solve?
Reasonix has two frontend runtimes — a TUI (Bubbletea) and a Desktop app (Wails + React) — that share a common
control.Controllerbackend. Over time, profiling revealed three categories of performance bottlenecks:Updatecounting lines, once inViewdisplaying them).Emit; no coalescing or backpressure exists; the React side rebuilds component trees on every update withoutuseDeferredValue.[]bytetostring; tool call arguments use+=for delta accumulation (O(n²)); shell command output accumulates via string concatenation; JSON parsing for the todo panel fires every frame regardless of whether the data changed; project files are re-read from disk on every access.These issues are most visible during long streaming conversations (>10k tokens), repeated tool call execution, and large shell command outputs — all common in daily use.
Proposed solution
Deliver three batches of zero-risk, independently verifiable optimisations across all four layers:
Batch 1 — Immediate gains (low risk):
stringoperations with zero-copy[]byte(scanner.Bytes()+bytes.HasPrefix/bytes.Equal/bytes.TrimSpace)Styleobjects at the package level, rebuilt only when width or colour changes (wrap, theme, queue indicator styles)mdRendererinstances per terminal width viasync.Map+=string concatenation withstrings.BuilderinconnectorBlock, MCP view width calculations, and shell output accumulationflushableMarkdownPrefixusing alastParagraphBreakbyte offsetloadProjectsFilewith a 5-second TTL andsync.RWMutex)streamedRowsrune scanning instead ofstrings.Split+ regexmap[event.Kind]stringwith a[KindCount]stringarray in the wire layerevent.Coalesce()as a shared exported function for continuous deltasBatch 2 — Core optimisations (medium risk):
wrapDirtyFrom+lineWrapCounts), only re-wrapping the dirty tail instead of the entire transcriptbuildStatusLinescaching (one call inUpdate, zero inView)renderRowsBuf/renderBarBuf/blankRow) inrenderTranscript, plusstrings.Builderoutput assemblyText/Reasoningdeltas, flush on non-coalescable events, async emitter with ring-buffer backpressure (drop oldest 1/4 at queue capacity 1024)memoonTranscript/StatusBar/UserMessagecomponents,useCallbackstabilisation forhandleToggleColdPage/handleToggleWarmTurn, 77useCallbackwrappers inApp.tsxdesktop/wire.go, shareeventwire.ToWire)scrollVersionkeyed on stable identifiers (id + kind + status) only, ignoring volatiletext.lengthreturn sguards inapplyEventreducer to return the same reference when nothing changedBatch 3 — Dependent / cross-cutting optimisations:
argsBuilders map[int]*strings.Builderin both OpenAI and Anthropic SSE handlerscoalesceEventDeltaswith a singlestrings.Builderper coalesced run, eliminating O(n²) string copy during long flush intervalstodoParsedArgs/todoParsedTodos, invalidated viasetTodoArgs/refreshTodoCache)buildBottomPanelsRowCount()called once inUpdateinstead of rendering twice)useDeferredValue(text)inMarkdownRendererto defer expensive markdown rendering during streaming; extractREMARK_PLUGINS/REHYPE_PLUGINSas module-level constantsAll optimisations preserve the
control.Controllertransport-agnostic contract, maintain existing test coverage, and are independently verifiable (go vet+go test+npx tsc).