Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions packages/tui/src/context/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
SkillInfo,
V2Event,
} from "@opencode-ai/sdk/v2"
import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
Expand Down Expand Up @@ -176,6 +177,30 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return created
}

// Delta batching: buffer incoming delta events for a short window and flush
// them in a single batched store update. This prevents UI freeze when large
// responses arrive in rapid bursts — each individual delta no longer triggers
// a separate re-render.
type DeltaEntry = { apply: () => void }
const deltaBuffer: DeltaEntry[] = []
let deltaTimer: ReturnType<typeof setTimeout> | undefined

function flushDeltaBuffer() {
deltaTimer = undefined
if (deltaBuffer.length === 0) return
const entries = deltaBuffer.splice(0)
batch(() => {
for (const entry of entries) entry.apply()
})
}

function bufferDelta(entry: DeltaEntry) {
deltaBuffer.push(entry)
if (!deltaTimer) {
deltaTimer = setTimeout(flushDeltaBuffer, 16)
}
}

// Walk parentID upward through loaded session info to the family root. When a
// parent's info is missing, that missing ID is the furthest-known ancestor and
// is returned so orphan subtrees group under it until the parent arrives. A
Expand Down Expand Up @@ -475,9 +500,13 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.text.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text += event.data.delta
bufferDelta({
apply() {
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text += event.data.delta
})
},
})
break
case "session.text.ended":
Expand All @@ -498,12 +527,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.tool.input.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID),
event.data.callID,
)
if (match?.state.status === "streaming") match.state.input += event.data.delta
bufferDelta({
apply() {
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID),
event.data.callID,
)
if (match?.state.status === "streaming") match.state.input += event.data.delta
})
},
})
break
case "session.tool.input.ended":
Expand Down Expand Up @@ -589,9 +622,13 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
case "session.reasoning.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text += event.data.delta
bufferDelta({
apply() {
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID))
if (match) match.text += event.data.delta
})
},
})
break
case "session.reasoning.ended":
Expand Down Expand Up @@ -1118,6 +1155,13 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}),
)

onCleanup(() => {
if (deltaTimer) {
clearTimeout(deltaTimer)
flushDeltaBuffer()
}
})

return result
},
})
Loading