Skip to content
Open
Show file tree
Hide file tree
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
50 changes: 32 additions & 18 deletions packages/opencode/src/cli/cmd/tui/context/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({

let queue: Event[] = []
let timer: Timer | undefined
let last = 0

const STREAM_BATCH_MS = 32
const EVENT_BATCH_MS = 12
const MAX_QUEUE_BEFORE_FLUSH = 100

const flush = () => {
if (queue.length === 0) return
const events = queue
queue = []
timer = undefined
last = Date.now()
// Batch all event emissions so all store updates result in a single render
batch(() => {
for (const event of events) {
Expand All @@ -49,16 +51,16 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({

const handleEvent = (event: Event) => {
queue.push(event)
const elapsed = Date.now() - last

if (timer) return
// If we just flushed recently (within 16ms), batch this with future events
// Otherwise, process immediately to avoid latency
if (elapsed < 16) {
timer = setTimeout(flush, 16)
if (queue.length >= MAX_QUEUE_BEFORE_FLUSH) {
if (timer) clearTimeout(timer)
flush()
return
}
flush()

if (timer) return
const wait = event.type === "message.part.updated" ? STREAM_BATCH_MS : EVENT_BATCH_MS
timer = setTimeout(flush, wait)
}

onMount(async () => {
Expand All @@ -70,17 +72,29 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
}

// Fall back to SSE
let retry = 250
while (true) {
if (abort.signal.aborted) break
const events = await sdk.event.subscribe(
{},
{
signal: abort.signal,
},
)

for await (const event of events.stream) {
handleEvent(event)
try {
const events = await sdk.event.subscribe(
{},
{
signal: abort.signal,
},
)

retry = 250
for await (const event of events.stream) {
handleEvent(event)
}

if (!abort.signal.aborted) {
await Bun.sleep(100)
}
} catch {
if (abort.signal.aborted) break
await Bun.sleep(retry)
retry = Math.min(retry * 2, 2_000)
}

// Flush any remaining events
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
if (parts[result.index] === event.properties.part) break
setStore("part", event.properties.part.messageID, result.index, event.properties.part)
break
}
setStore(
Expand Down
13 changes: 5 additions & 8 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1354,25 +1354,22 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
const ctx = use()
const { theme, syntax } = useTheme()
const content = createMemo(() => props.part.text)

return (
<Show when={props.part.text.trim()}>
<Show when={content().length > 0}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<Switch>
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<markdown
syntaxStyle={syntax()}
streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
<markdown syntaxStyle={syntax()} streaming={true} content={content()} conceal={ctx.conceal()} />
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
content={content()}
conceal={ctx.conceal()}
fg={theme.text}
/>
Expand Down
Loading