fix(app): better tool call batching
This commit is contained in:
@@ -117,6 +117,7 @@ function createThrottledValue(getValue: () => string) {
|
||||
createEffect(() => {
|
||||
const next = getValue()
|
||||
const now = Date.now()
|
||||
|
||||
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
|
||||
if (remaining <= 0) {
|
||||
if (timeout) {
|
||||
@@ -250,6 +251,126 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
|
||||
}
|
||||
|
||||
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
|
||||
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
|
||||
|
||||
function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
||||
if (Array.isArray(value)) return value
|
||||
return fallback
|
||||
}
|
||||
|
||||
function renderable(part: PartType) {
|
||||
if (part.type === "tool") {
|
||||
if (HIDDEN_TOOLS.has(part.tool)) return false
|
||||
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
||||
return true
|
||||
}
|
||||
if (part.type === "text") return !!part.text?.trim()
|
||||
if (part.type === "reasoning") return !!part.text?.trim()
|
||||
return !!PART_MAPPING[part.type]
|
||||
}
|
||||
|
||||
export function AssistantParts(props: {
|
||||
messages: AssistantMessage[]
|
||||
showAssistantCopyPartID?: string | null
|
||||
working?: boolean
|
||||
}) {
|
||||
const data = useData()
|
||||
const emptyParts: PartType[] = []
|
||||
|
||||
const grouped = createMemo(() => {
|
||||
const keys: string[] = []
|
||||
const items: Record<
|
||||
string,
|
||||
{ type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] }
|
||||
> = {}
|
||||
const push = (
|
||||
key: string,
|
||||
item: { type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] },
|
||||
) => {
|
||||
keys.push(key)
|
||||
items[key] = item
|
||||
}
|
||||
|
||||
const parts = props.messages.flatMap((message) =>
|
||||
list(data.store.part?.[message.id], emptyParts)
|
||||
.filter(renderable)
|
||||
.map((part) => ({ message, part })),
|
||||
)
|
||||
|
||||
let start = -1
|
||||
|
||||
const flush = (end: number) => {
|
||||
if (start < 0) return
|
||||
const first = parts[start]
|
||||
const last = parts[end]
|
||||
if (!first || !last) {
|
||||
start = -1
|
||||
return
|
||||
}
|
||||
push(`context:${first.part.id}`, {
|
||||
type: "context",
|
||||
parts: parts
|
||||
.slice(start, end + 1)
|
||||
.map((x) => x.part)
|
||||
.filter((part): part is ToolPart => isContextGroupTool(part)),
|
||||
})
|
||||
start = -1
|
||||
}
|
||||
|
||||
parts.forEach((item, index) => {
|
||||
if (isContextGroupTool(item.part)) {
|
||||
if (start < 0) start = index
|
||||
return
|
||||
}
|
||||
|
||||
flush(index - 1)
|
||||
push(`part:${item.message.id}:${item.part.id}`, { type: "part", part: item.part, message: item.message })
|
||||
})
|
||||
|
||||
flush(parts.length - 1)
|
||||
|
||||
return { keys, items }
|
||||
})
|
||||
|
||||
const last = createMemo(() => grouped().keys.at(-1))
|
||||
|
||||
return (
|
||||
<For each={grouped().keys}>
|
||||
{(key) => {
|
||||
const item = createMemo(() => grouped().items[key])
|
||||
const ctx = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "context") return
|
||||
return value
|
||||
})
|
||||
const part = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "part") return
|
||||
return value
|
||||
})
|
||||
const tail = createMemo(() => last() === key)
|
||||
return (
|
||||
<>
|
||||
<Show when={ctx()}>
|
||||
{(entry) => <ContextToolGroup parts={entry().parts} busy={props.working && tail()} />}
|
||||
</Show>
|
||||
<Show when={part()}>
|
||||
{(entry) => (
|
||||
<Part
|
||||
part={entry().part}
|
||||
message={entry().message}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
|
||||
function isContextGroupTool(part: PartType): part is ToolPart {
|
||||
return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool)
|
||||
@@ -390,6 +511,8 @@ export function AssistantMessageDisplay(props: {
|
||||
}
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!renderable(part)) return
|
||||
|
||||
if (isContextGroupTool(part)) {
|
||||
if (start < 0) start = index
|
||||
return
|
||||
@@ -408,31 +531,43 @@ export function AssistantMessageDisplay(props: {
|
||||
<For each={grouped().keys}>
|
||||
{(key) => {
|
||||
const item = createMemo(() => grouped().items[key])
|
||||
const ctx = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "context") return
|
||||
return value
|
||||
})
|
||||
const part = createMemo(() => {
|
||||
const value = item()
|
||||
if (!value) return
|
||||
if (value.type !== "part") return
|
||||
return value
|
||||
})
|
||||
return (
|
||||
<Show when={item()}>
|
||||
{(value) => {
|
||||
const entry = value()
|
||||
if (entry.type === "context") return <ContextToolGroup parts={entry.parts} />
|
||||
return (
|
||||
<>
|
||||
<Show when={ctx()}>{(entry) => <ContextToolGroup parts={entry().parts} />}</Show>
|
||||
<Show when={part()}>
|
||||
{(entry) => (
|
||||
<Part
|
||||
part={entry.part}
|
||||
part={entry().part}
|
||||
message={props.message}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextToolGroup(props: { parts: ToolPart[] }) {
|
||||
function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||
const i18n = useI18n()
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const pending = createMemo(() =>
|
||||
props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
const pending = createMemo(
|
||||
() =>
|
||||
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
)
|
||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||
const details = createMemo(() => summary().join(", "))
|
||||
@@ -445,7 +580,7 @@ function ContextToolGroup(props: { parts: ToolPart[] }) {
|
||||
when={pending()}
|
||||
fallback={
|
||||
<span data-slot="context-tool-group-title">
|
||||
<span data-slot="context-tool-group-label">Gathered context</span>
|
||||
<span data-slot="context-tool-group-label">{i18n.t("ui.sessionTurn.status.gatheredContext")}</span>
|
||||
<Show when={details().length}>
|
||||
<span data-slot="context-tool-group-summary">{details()}</span>
|
||||
</Show>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { createMemo, createSignal, For, ParentProps, Show } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { Message } from "./message-part"
|
||||
import { AssistantParts, Message } from "./message-part"
|
||||
import { Card } from "./card"
|
||||
import { Collapsible } from "./collapsible"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
@@ -91,13 +91,6 @@ function visible(part: PartType) {
|
||||
return false
|
||||
}
|
||||
|
||||
function AssistantMessageItem(props: { message: AssistantMessage; showAssistantCopyPartID?: string | null }) {
|
||||
const data = useData()
|
||||
const emptyParts: PartType[] = []
|
||||
const msgParts = createMemo(() => list(data.store.part?.[props.message.id], emptyParts))
|
||||
return <Message message={props.message} parts={msgParts()} showAssistantCopyPartID={props.showAssistantCopyPartID} />
|
||||
}
|
||||
|
||||
export function SessionTurn(
|
||||
props: ParentProps<{
|
||||
sessionID: string
|
||||
@@ -237,8 +230,7 @@ export function SessionTurn(
|
||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||
|
||||
const assistantCopyPartID = createMemo(() => {
|
||||
if (!isLastUserMessage()) return null
|
||||
if (status().type !== "idle") return null
|
||||
if (working()) return null
|
||||
return showAssistantCopyPartID() ?? null
|
||||
})
|
||||
const assistantVisible = createMemo(() =>
|
||||
@@ -281,14 +273,11 @@ export function SessionTurn(
|
||||
</Show>
|
||||
<Show when={assistantMessages().length > 0}>
|
||||
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
||||
<For each={assistantMessages()}>
|
||||
{(assistantMessage) => (
|
||||
<AssistantMessageItem
|
||||
message={assistantMessage}
|
||||
showAssistantCopyPartID={assistantCopyPartID()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<AssistantParts
|
||||
messages={assistantMessages()}
|
||||
showAssistantCopyPartID={assistantCopyPartID()}
|
||||
working={working()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={edited() > 0}>
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "تفويض العمل",
|
||||
"ui.sessionTurn.status.planning": "تخطيط الخطوات التالية",
|
||||
"ui.sessionTurn.status.gatheringContext": "جمع السياق",
|
||||
"ui.sessionTurn.status.gatheringContext": "استكشاف...",
|
||||
"ui.sessionTurn.status.gatheredContext": "تم الاستكشاف",
|
||||
"ui.sessionTurn.status.searchingCodebase": "البحث في قاعدة التعليمات البرمجية",
|
||||
"ui.sessionTurn.status.searchingWeb": "البحث في الويب",
|
||||
"ui.sessionTurn.status.makingEdits": "إجراء تعديلات",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegando trabalho",
|
||||
"ui.sessionTurn.status.planning": "Planejando próximos passos",
|
||||
"ui.sessionTurn.status.gatheringContext": "Coletando contexto",
|
||||
"ui.sessionTurn.status.gatheringContext": "Explorando...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Explorado",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Pesquisando no código",
|
||||
"ui.sessionTurn.status.searchingWeb": "Pesquisando na web",
|
||||
"ui.sessionTurn.status.makingEdits": "Fazendo edições",
|
||||
|
||||
@@ -37,7 +37,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegiranje posla",
|
||||
"ui.sessionTurn.status.planning": "Planiranje sljedećih koraka",
|
||||
"ui.sessionTurn.status.gatheringContext": "Prikupljanje konteksta",
|
||||
"ui.sessionTurn.status.gatheringContext": "Istraživanje...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Istraženo",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Pretraživanje baze koda",
|
||||
"ui.sessionTurn.status.searchingWeb": "Pretraživanje weba",
|
||||
"ui.sessionTurn.status.makingEdits": "Pravljenje izmjena",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegerer arbejde",
|
||||
"ui.sessionTurn.status.planning": "Planlægger næste trin",
|
||||
"ui.sessionTurn.status.gatheringContext": "Indsamler kontekst",
|
||||
"ui.sessionTurn.status.gatheringContext": "Udforsker...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Udforsket",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Søger i koden",
|
||||
"ui.sessionTurn.status.searchingWeb": "Søger på nettet",
|
||||
"ui.sessionTurn.status.makingEdits": "Laver ændringer",
|
||||
|
||||
@@ -36,7 +36,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Arbeit delegieren",
|
||||
"ui.sessionTurn.status.planning": "Nächste Schritte planen",
|
||||
"ui.sessionTurn.status.gatheringContext": "Kontext sammeln",
|
||||
"ui.sessionTurn.status.gatheringContext": "Erkunden...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Erkundet",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Codebasis durchsuchen",
|
||||
"ui.sessionTurn.status.searchingWeb": "Web durchsuchen",
|
||||
"ui.sessionTurn.status.makingEdits": "Änderungen vornehmen",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegating work",
|
||||
"ui.sessionTurn.status.planning": "Planning next steps",
|
||||
"ui.sessionTurn.status.gatheringContext": "Gathering context",
|
||||
"ui.sessionTurn.status.gatheringContext": "Exploring...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Explored",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Searching the codebase",
|
||||
"ui.sessionTurn.status.searchingWeb": "Searching the web",
|
||||
"ui.sessionTurn.status.makingEdits": "Making edits",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegando trabajo",
|
||||
"ui.sessionTurn.status.planning": "Planificando siguientes pasos",
|
||||
"ui.sessionTurn.status.gatheringContext": "Recopilando contexto",
|
||||
"ui.sessionTurn.status.gatheringContext": "Explorando...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Explorado",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Buscando en la base de código",
|
||||
"ui.sessionTurn.status.searchingWeb": "Buscando en la web",
|
||||
"ui.sessionTurn.status.makingEdits": "Realizando ediciones",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Délégation du travail",
|
||||
"ui.sessionTurn.status.planning": "Planification des prochaines étapes",
|
||||
"ui.sessionTurn.status.gatheringContext": "Collecte du contexte",
|
||||
"ui.sessionTurn.status.gatheringContext": "Exploration...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Exploré",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Recherche dans la base de code",
|
||||
"ui.sessionTurn.status.searchingWeb": "Recherche sur le web",
|
||||
"ui.sessionTurn.status.makingEdits": "Application des modifications",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "作業を委任中",
|
||||
"ui.sessionTurn.status.planning": "次のステップを計画中",
|
||||
"ui.sessionTurn.status.gatheringContext": "コンテキストを収集中",
|
||||
"ui.sessionTurn.status.gatheringContext": "探索中...",
|
||||
"ui.sessionTurn.status.gatheredContext": "探索済み",
|
||||
"ui.sessionTurn.status.searchingCodebase": "コードベースを検索中",
|
||||
"ui.sessionTurn.status.searchingWeb": "ウェブを検索中",
|
||||
"ui.sessionTurn.status.makingEdits": "編集を実行中",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "작업 위임 중",
|
||||
"ui.sessionTurn.status.planning": "다음 단계 계획 중",
|
||||
"ui.sessionTurn.status.gatheringContext": "컨텍스트 수집 중",
|
||||
"ui.sessionTurn.status.gatheringContext": "탐색 중...",
|
||||
"ui.sessionTurn.status.gatheredContext": "탐색됨",
|
||||
"ui.sessionTurn.status.searchingCodebase": "코드베이스 검색 중",
|
||||
"ui.sessionTurn.status.searchingWeb": "웹 검색 중",
|
||||
"ui.sessionTurn.status.makingEdits": "편집 수행 중",
|
||||
|
||||
@@ -36,7 +36,8 @@ export const dict: Record<Keys, string> = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegerer arbeid",
|
||||
"ui.sessionTurn.status.planning": "Planlegger neste trinn",
|
||||
"ui.sessionTurn.status.gatheringContext": "Samler inn kontekst",
|
||||
"ui.sessionTurn.status.gatheringContext": "Utforsker...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Utforsket",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Søker i kodebasen",
|
||||
"ui.sessionTurn.status.searchingWeb": "Søker på nettet",
|
||||
"ui.sessionTurn.status.makingEdits": "Gjør endringer",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegowanie pracy",
|
||||
"ui.sessionTurn.status.planning": "Planowanie kolejnych kroków",
|
||||
"ui.sessionTurn.status.gatheringContext": "Zbieranie kontekstu",
|
||||
"ui.sessionTurn.status.gatheringContext": "Eksplorowanie...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Wyeksplorowano",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Przeszukiwanie bazy kodu",
|
||||
"ui.sessionTurn.status.searchingWeb": "Przeszukiwanie sieci",
|
||||
"ui.sessionTurn.status.makingEdits": "Wprowadzanie zmian",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Делегирование работы",
|
||||
"ui.sessionTurn.status.planning": "Планирование следующих шагов",
|
||||
"ui.sessionTurn.status.gatheringContext": "Сбор контекста",
|
||||
"ui.sessionTurn.status.gatheringContext": "Исследование...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Исследовано",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Поиск в кодовой базе",
|
||||
"ui.sessionTurn.status.searchingWeb": "Поиск в интернете",
|
||||
"ui.sessionTurn.status.makingEdits": "Внесение изменений",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "มอบหมายงาน",
|
||||
"ui.sessionTurn.status.planning": "วางแผนขั้นตอนถัดไป",
|
||||
"ui.sessionTurn.status.gatheringContext": "รวบรวมบริบท",
|
||||
"ui.sessionTurn.status.gatheringContext": "กำลังสำรวจ...",
|
||||
"ui.sessionTurn.status.gatheredContext": "สำรวจแล้ว",
|
||||
"ui.sessionTurn.status.searchingCodebase": "กำลังค้นหาโค้ดเบส",
|
||||
"ui.sessionTurn.status.searchingWeb": "กำลังค้นหาบนเว็บ",
|
||||
"ui.sessionTurn.status.makingEdits": "กำลังแก้ไข",
|
||||
|
||||
@@ -37,7 +37,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "正在委派工作",
|
||||
"ui.sessionTurn.status.planning": "正在规划下一步",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在收集上下文",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在探索...",
|
||||
"ui.sessionTurn.status.gatheredContext": "已探索",
|
||||
"ui.sessionTurn.status.searchingCodebase": "正在搜索代码库",
|
||||
"ui.sessionTurn.status.searchingWeb": "正在搜索网页",
|
||||
"ui.sessionTurn.status.makingEdits": "正在修改",
|
||||
|
||||
@@ -37,7 +37,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "正在委派工作",
|
||||
"ui.sessionTurn.status.planning": "正在規劃下一步",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在收集上下文",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在探索...",
|
||||
"ui.sessionTurn.status.gatheredContext": "已探索",
|
||||
"ui.sessionTurn.status.searchingCodebase": "正在搜尋程式碼庫",
|
||||
"ui.sessionTurn.status.searchingWeb": "正在搜尋網頁",
|
||||
"ui.sessionTurn.status.makingEdits": "正在修改",
|
||||
|
||||
Reference in New Issue
Block a user