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
58 changes: 48 additions & 10 deletions packages/tui/src/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,41 @@ export const {
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
}

// Fast-streaming models emit message.part.delta faster than the renderer
// keeps up; applying every chunk synchronously starves input handling and
// freezes scrolling (#36043). Deltas accumulate per part field and flush
// together on a short interval instead.
const pendingDeltas = new Map<string, { messageID: string; partID: string; field: string; delta: string }>()
let deltaTimer: ReturnType<typeof setTimeout> | undefined
const dropPendingDeltas = (messageID: string, partID: string) => {
if (!pendingDeltas.size) return
const prefix = `${messageID}|${partID}|`
for (const key of pendingDeltas.keys()) if (key.startsWith(prefix)) pendingDeltas.delete(key)
}
const flushDeltas = () => {
deltaTimer = undefined
if (!pendingDeltas.size) return
batch(() => {
for (const pending of pendingDeltas.values()) {
const parts = store.part[pending.messageID]
if (!parts) continue
const result = search(parts, pending.partID, (p) => p.id)
if (!result.found) continue
setStore(
"part",
pending.messageID,
produce((draft) => {
const part = draft[result.index]
const field = pending.field as keyof typeof part
const existing = part[field] as string | undefined
;(part[field] as string) = (existing ?? "") + pending.delta
}),
)
}
pendingDeltas.clear()
})
}

event.subscribe((event, { directory, workspace }) => {
switch (event.type) {
case "server.instance.disposed":
Expand Down Expand Up @@ -368,6 +403,8 @@ export const {
break
}
case "message.part.updated": {
// full snapshot supersedes any buffered deltas for this part
dropPendingDeltas(event.properties.part.messageID, event.properties.part.id)
touchPart(event.properties.part.sessionID, event.properties.part.id)
const parts = store.part[event.properties.part.messageID]
if (!parts) {
Expand Down Expand Up @@ -395,16 +432,17 @@ export const {
const result = search(parts, event.properties.partID, (p) => p.id)
if (!result.found) break
touchPart(event.properties.sessionID, event.properties.partID)
setStore(
"part",
event.properties.messageID,
produce((draft) => {
const part = draft[result.index]
const field = event.properties.field as keyof typeof part
const existing = part[field] as string | undefined
;(part[field] as string) = (existing ?? "") + event.properties.delta
}),
)
const key = `${event.properties.messageID}|${event.properties.partID}|${event.properties.field}`
const pending = pendingDeltas.get(key)
if (pending) pending.delta += event.properties.delta
else
pendingDeltas.set(key, {
messageID: event.properties.messageID,
partID: event.properties.partID,
field: event.properties.field,
delta: event.properties.delta,
})
if (!deltaTimer) deltaTimer = setTimeout(flushDeltas, 40)
break
}

Expand Down
Loading