Skip to content

Commit e345b89

Browse files
committed
fix(app): better tool call batching
1 parent 26c7b24 commit e345b89

18 files changed

Lines changed: 187 additions & 47 deletions

File tree

packages/ui/src/components/message-part.tsx

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function createThrottledValue(getValue: () => string) {
117117
createEffect(() => {
118118
const next = getValue()
119119
const now = Date.now()
120+
120121
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
121122
if (remaining <= 0) {
122123
if (timeout) {
@@ -250,6 +251,126 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
250251
}
251252

252253
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
254+
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
255+
256+
function list<T>(value: T[] | undefined | null, fallback: T[]) {
257+
if (Array.isArray(value)) return value
258+
return fallback
259+
}
260+
261+
function renderable(part: PartType) {
262+
if (part.type === "tool") {
263+
if (HIDDEN_TOOLS.has(part.tool)) return false
264+
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
265+
return true
266+
}
267+
if (part.type === "text") return !!part.text?.trim()
268+
if (part.type === "reasoning") return !!part.text?.trim()
269+
return !!PART_MAPPING[part.type]
270+
}
271+
272+
export function AssistantParts(props: {
273+
messages: AssistantMessage[]
274+
showAssistantCopyPartID?: string | null
275+
working?: boolean
276+
}) {
277+
const data = useData()
278+
const emptyParts: PartType[] = []
279+
280+
const grouped = createMemo(() => {
281+
const keys: string[] = []
282+
const items: Record<
283+
string,
284+
{ type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] }
285+
> = {}
286+
const push = (
287+
key: string,
288+
item: { type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] },
289+
) => {
290+
keys.push(key)
291+
items[key] = item
292+
}
293+
294+
const parts = props.messages.flatMap((message) =>
295+
list(data.store.part?.[message.id], emptyParts)
296+
.filter(renderable)
297+
.map((part) => ({ message, part })),
298+
)
299+
300+
let start = -1
301+
302+
const flush = (end: number) => {
303+
if (start < 0) return
304+
const first = parts[start]
305+
const last = parts[end]
306+
if (!first || !last) {
307+
start = -1
308+
return
309+
}
310+
push(`context:${first.part.id}`, {
311+
type: "context",
312+
parts: parts
313+
.slice(start, end + 1)
314+
.map((x) => x.part)
315+
.filter((part): part is ToolPart => isContextGroupTool(part)),
316+
})
317+
start = -1
318+
}
319+
320+
parts.forEach((item, index) => {
321+
if (isContextGroupTool(item.part)) {
322+
if (start < 0) start = index
323+
return
324+
}
325+
326+
flush(index - 1)
327+
push(`part:${item.message.id}:${item.part.id}`, { type: "part", part: item.part, message: item.message })
328+
})
329+
330+
flush(parts.length - 1)
331+
332+
return { keys, items }
333+
})
334+
335+
const last = createMemo(() => grouped().keys.at(-1))
336+
337+
return (
338+
<For each={grouped().keys}>
339+
{(key) => {
340+
const item = createMemo(() => grouped().items[key])
341+
const ctx = createMemo(() => {
342+
const value = item()
343+
if (!value) return
344+
if (value.type !== "context") return
345+
return value
346+
})
347+
const part = createMemo(() => {
348+
const value = item()
349+
if (!value) return
350+
if (value.type !== "part") return
351+
return value
352+
})
353+
const tail = createMemo(() => last() === key)
354+
return (
355+
<>
356+
<Show when={ctx()}>
357+
{(entry) => <ContextToolGroup parts={entry().parts} busy={props.working && tail()} />}
358+
</Show>
359+
<Show when={part()}>
360+
{(entry) => (
361+
<Part
362+
part={entry().part}
363+
message={entry().message}
364+
showAssistantCopyPartID={props.showAssistantCopyPartID}
365+
/>
366+
)}
367+
</Show>
368+
</>
369+
)
370+
}}
371+
</For>
372+
)
373+
}
253374

254375
function isContextGroupTool(part: PartType): part is ToolPart {
255376
return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool)
@@ -390,6 +511,8 @@ export function AssistantMessageDisplay(props: {
390511
}
391512

392513
parts.forEach((part, index) => {
514+
if (!renderable(part)) return
515+
393516
if (isContextGroupTool(part)) {
394517
if (start < 0) start = index
395518
return
@@ -408,31 +531,43 @@ export function AssistantMessageDisplay(props: {
408531
<For each={grouped().keys}>
409532
{(key) => {
410533
const item = createMemo(() => grouped().items[key])
534+
const ctx = createMemo(() => {
535+
const value = item()
536+
if (!value) return
537+
if (value.type !== "context") return
538+
return value
539+
})
540+
const part = createMemo(() => {
541+
const value = item()
542+
if (!value) return
543+
if (value.type !== "part") return
544+
return value
545+
})
411546
return (
412-
<Show when={item()}>
413-
{(value) => {
414-
const entry = value()
415-
if (entry.type === "context") return <ContextToolGroup parts={entry.parts} />
416-
return (
547+
<>
548+
<Show when={ctx()}>{(entry) => <ContextToolGroup parts={entry().parts} />}</Show>
549+
<Show when={part()}>
550+
{(entry) => (
417551
<Part
418-
part={entry.part}
552+
part={entry().part}
419553
message={props.message}
420554
showAssistantCopyPartID={props.showAssistantCopyPartID}
421555
/>
422-
)
423-
}}
424-
</Show>
556+
)}
557+
</Show>
558+
</>
425559
)
426560
}}
427561
</For>
428562
)
429563
}
430564

431-
function ContextToolGroup(props: { parts: ToolPart[] }) {
565+
function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
432566
const i18n = useI18n()
433567
const [open, setOpen] = createSignal(false)
434-
const pending = createMemo(() =>
435-
props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
568+
const pending = createMemo(
569+
() =>
570+
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
436571
)
437572
const summary = createMemo(() => contextToolSummary(props.parts))
438573
const details = createMemo(() => summary().join(", "))
@@ -445,7 +580,7 @@ function ContextToolGroup(props: { parts: ToolPart[] }) {
445580
when={pending()}
446581
fallback={
447582
<span data-slot="context-tool-group-title">
448-
<span data-slot="context-tool-group-label">Gathered context</span>
583+
<span data-slot="context-tool-group-label">{i18n.t("ui.sessionTurn.status.gatheredContext")}</span>
449584
<Show when={details().length}>
450585
<span data-slot="context-tool-group-summary">{details()}</span>
451586
</Show>

packages/ui/src/components/session-turn.tsx

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
66
import { getDirectory, getFilename } from "@opencode-ai/util/path"
77
import { createMemo, createSignal, For, ParentProps, Show } from "solid-js"
88
import { Dynamic } from "solid-js/web"
9-
import { Message } from "./message-part"
9+
import { AssistantParts, Message } from "./message-part"
1010
import { Card } from "./card"
1111
import { Collapsible } from "./collapsible"
1212
import { DiffChanges } from "./diff-changes"
@@ -91,13 +91,6 @@ function visible(part: PartType) {
9191
return false
9292
}
9393

94-
function AssistantMessageItem(props: { message: AssistantMessage; showAssistantCopyPartID?: string | null }) {
95-
const data = useData()
96-
const emptyParts: PartType[] = []
97-
const msgParts = createMemo(() => list(data.store.part?.[props.message.id], emptyParts))
98-
return <Message message={props.message} parts={msgParts()} showAssistantCopyPartID={props.showAssistantCopyPartID} />
99-
}
100-
10194
export function SessionTurn(
10295
props: ParentProps<{
10396
sessionID: string
@@ -237,8 +230,7 @@ export function SessionTurn(
237230
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
238231

239232
const assistantCopyPartID = createMemo(() => {
240-
if (!isLastUserMessage()) return null
241-
if (status().type !== "idle") return null
233+
if (working()) return null
242234
return showAssistantCopyPartID() ?? null
243235
})
244236
const assistantVisible = createMemo(() =>
@@ -281,14 +273,11 @@ export function SessionTurn(
281273
</Show>
282274
<Show when={assistantMessages().length > 0}>
283275
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
284-
<For each={assistantMessages()}>
285-
{(assistantMessage) => (
286-
<AssistantMessageItem
287-
message={assistantMessage}
288-
showAssistantCopyPartID={assistantCopyPartID()}
289-
/>
290-
)}
291-
</For>
276+
<AssistantParts
277+
messages={assistantMessages()}
278+
showAssistantCopyPartID={assistantCopyPartID()}
279+
working={working()}
280+
/>
292281
</div>
293282
</Show>
294283
<Show when={edited() > 0}>

packages/ui/src/i18n/ar.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const dict = {
3333

3434
"ui.sessionTurn.status.delegating": "تفويض العمل",
3535
"ui.sessionTurn.status.planning": "تخطيط الخطوات التالية",
36-
"ui.sessionTurn.status.gatheringContext": "جمع السياق",
36+
"ui.sessionTurn.status.gatheringContext": "استكشاف...",
37+
"ui.sessionTurn.status.gatheredContext": "تم الاستكشاف",
3738
"ui.sessionTurn.status.searchingCodebase": "البحث في قاعدة التعليمات البرمجية",
3839
"ui.sessionTurn.status.searchingWeb": "البحث في الويب",
3940
"ui.sessionTurn.status.makingEdits": "إجراء تعديلات",

packages/ui/src/i18n/br.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const dict = {
3333

3434
"ui.sessionTurn.status.delegating": "Delegando trabalho",
3535
"ui.sessionTurn.status.planning": "Planejando próximos passos",
36-
"ui.sessionTurn.status.gatheringContext": "Coletando contexto",
36+
"ui.sessionTurn.status.gatheringContext": "Explorando...",
37+
"ui.sessionTurn.status.gatheredContext": "Explorado",
3738
"ui.sessionTurn.status.searchingCodebase": "Pesquisando no código",
3839
"ui.sessionTurn.status.searchingWeb": "Pesquisando na web",
3940
"ui.sessionTurn.status.makingEdits": "Fazendo edições",

packages/ui/src/i18n/bs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export const dict = {
3737

3838
"ui.sessionTurn.status.delegating": "Delegiranje posla",
3939
"ui.sessionTurn.status.planning": "Planiranje sljedećih koraka",
40-
"ui.sessionTurn.status.gatheringContext": "Prikupljanje konteksta",
40+
"ui.sessionTurn.status.gatheringContext": "Istraživanje...",
41+
"ui.sessionTurn.status.gatheredContext": "Istraženo",
4142
"ui.sessionTurn.status.searchingCodebase": "Pretraživanje baze koda",
4243
"ui.sessionTurn.status.searchingWeb": "Pretraživanje weba",
4344
"ui.sessionTurn.status.makingEdits": "Pravljenje izmjena",

packages/ui/src/i18n/da.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export const dict = {
3232

3333
"ui.sessionTurn.status.delegating": "Delegerer arbejde",
3434
"ui.sessionTurn.status.planning": "Planlægger næste trin",
35-
"ui.sessionTurn.status.gatheringContext": "Indsamler kontekst",
35+
"ui.sessionTurn.status.gatheringContext": "Udforsker...",
36+
"ui.sessionTurn.status.gatheredContext": "Udforsket",
3637
"ui.sessionTurn.status.searchingCodebase": "Søger i koden",
3738
"ui.sessionTurn.status.searchingWeb": "Søger på nettet",
3839
"ui.sessionTurn.status.makingEdits": "Laver ændringer",

packages/ui/src/i18n/de.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export const dict = {
3636

3737
"ui.sessionTurn.status.delegating": "Arbeit delegieren",
3838
"ui.sessionTurn.status.planning": "Nächste Schritte planen",
39-
"ui.sessionTurn.status.gatheringContext": "Kontext sammeln",
39+
"ui.sessionTurn.status.gatheringContext": "Erkunden...",
40+
"ui.sessionTurn.status.gatheredContext": "Erkundet",
4041
"ui.sessionTurn.status.searchingCodebase": "Codebasis durchsuchen",
4142
"ui.sessionTurn.status.searchingWeb": "Web durchsuchen",
4243
"ui.sessionTurn.status.makingEdits": "Änderungen vornehmen",

packages/ui/src/i18n/en.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const dict = {
3333

3434
"ui.sessionTurn.status.delegating": "Delegating work",
3535
"ui.sessionTurn.status.planning": "Planning next steps",
36-
"ui.sessionTurn.status.gatheringContext": "Gathering context",
36+
"ui.sessionTurn.status.gatheringContext": "Exploring...",
37+
"ui.sessionTurn.status.gatheredContext": "Explored",
3738
"ui.sessionTurn.status.searchingCodebase": "Searching the codebase",
3839
"ui.sessionTurn.status.searchingWeb": "Searching the web",
3940
"ui.sessionTurn.status.makingEdits": "Making edits",

packages/ui/src/i18n/es.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const dict = {
3333

3434
"ui.sessionTurn.status.delegating": "Delegando trabajo",
3535
"ui.sessionTurn.status.planning": "Planificando siguientes pasos",
36-
"ui.sessionTurn.status.gatheringContext": "Recopilando contexto",
36+
"ui.sessionTurn.status.gatheringContext": "Explorando...",
37+
"ui.sessionTurn.status.gatheredContext": "Explorado",
3738
"ui.sessionTurn.status.searchingCodebase": "Buscando en la base de código",
3839
"ui.sessionTurn.status.searchingWeb": "Buscando en la web",
3940
"ui.sessionTurn.status.makingEdits": "Realizando ediciones",

packages/ui/src/i18n/fr.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const dict = {
3333

3434
"ui.sessionTurn.status.delegating": "Délégation du travail",
3535
"ui.sessionTurn.status.planning": "Planification des prochaines étapes",
36-
"ui.sessionTurn.status.gatheringContext": "Collecte du contexte",
36+
"ui.sessionTurn.status.gatheringContext": "Exploration...",
37+
"ui.sessionTurn.status.gatheredContext": "Exploré",
3738
"ui.sessionTurn.status.searchingCodebase": "Recherche dans la base de code",
3839
"ui.sessionTurn.status.searchingWeb": "Recherche sur le web",
3940
"ui.sessionTurn.status.makingEdits": "Application des modifications",

0 commit comments

Comments
 (0)