tui: dismiss question when tool part ends
Questions remained visible after the underlying tool call completed or errored because nothing removed them from the store. Clear each question once its associated tool part reaches a terminal state, and fix recovery to preserve correct blocker priority across all active questions.
This commit is contained in:
@@ -530,16 +530,19 @@ function createLayer(input: StreamInput) {
|
||||
|
||||
const matching = questions.filter(matches)
|
||||
if (matching.length > 0) {
|
||||
state.data.questions = state.data.questions.filter(matches)
|
||||
const active = new Set(questions.map((request) => request.id))
|
||||
state.data.questions = state.data.questions.filter((request) => active.has(request.id))
|
||||
bootstrapSessionData({
|
||||
data: state.data,
|
||||
messages: [],
|
||||
permissions: [],
|
||||
questions: matching,
|
||||
questions,
|
||||
})
|
||||
for (const request of matching) {
|
||||
for (const request of questions) {
|
||||
seedBlocker(request.id)
|
||||
}
|
||||
const priority = Math.min(0, ...state.blockers.values()) - 1
|
||||
for (const request of matching) state.blockers.set(request.id, priority)
|
||||
input.trace?.write("question.recover", {
|
||||
sessionID: input.sessionID,
|
||||
requests: matching.map((request) => request.id),
|
||||
|
||||
@@ -34,6 +34,14 @@ import path from "path"
|
||||
import { useKV } from "./kv"
|
||||
import { aggregateFailures } from "./aggregate-failures"
|
||||
|
||||
export function questionToolRequestIndex(requests: readonly QuestionRequest[] | undefined, part: Part) {
|
||||
if (part.type !== "tool") return -1
|
||||
if (part.state.status !== "completed" && part.state.status !== "error") return -1
|
||||
return requests?.findIndex(
|
||||
(request) => request.tool?.messageID === part.messageID && request.tool?.callID === part.callID,
|
||||
) ?? -1
|
||||
}
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
init: () => {
|
||||
@@ -304,23 +312,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
const parts = store.part[event.properties.part.messageID]
|
||||
const part = event.properties.part
|
||||
const parts = store.part[part.messageID]
|
||||
if (!parts) {
|
||||
setStore("part", event.properties.part.messageID, [event.properties.part])
|
||||
break
|
||||
setStore("part", part.messageID, [part])
|
||||
}
|
||||
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))
|
||||
break
|
||||
if (parts) {
|
||||
const result = Binary.search(parts, part.id, (p) => p.id)
|
||||
if (result.found) setStore("part", part.messageID, result.index, reconcile(part))
|
||||
if (!result.found)
|
||||
setStore(
|
||||
"part",
|
||||
part.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, part)
|
||||
}),
|
||||
)
|
||||
}
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.part.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.part)
|
||||
}),
|
||||
)
|
||||
const index = questionToolRequestIndex(store.question[part.sessionID], part)
|
||||
if (index !== -1)
|
||||
setStore(
|
||||
"question",
|
||||
part.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -835,7 +835,7 @@ describe("run stream transport", () => {
|
||||
callID: "call-question-1",
|
||||
},
|
||||
}
|
||||
const stale = {
|
||||
const other = {
|
||||
...request,
|
||||
id: "question-old",
|
||||
tool: { messageID: "msg-old", callID: "call-question-old" },
|
||||
@@ -845,7 +845,7 @@ describe("run stream transport", () => {
|
||||
stream: src.stream,
|
||||
questions: async () => {
|
||||
questionCalls += 1
|
||||
return ok(questionCalls === 1 ? [stale] : [stale, request])
|
||||
return ok(questionCalls === 1 ? [other] : [other, request])
|
||||
},
|
||||
promptAsync: async () => {
|
||||
queueMicrotask(() => {
|
||||
@@ -930,11 +930,13 @@ describe("run stream transport", () => {
|
||||
expect(
|
||||
await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.view")
|
||||
return item?.type === "stream.view" && item.view.type === "prompt" ? item : undefined
|
||||
return item?.type === "stream.view" && item.view.type === "question" && item.view.request.id === other.id
|
||||
? item.view
|
||||
: undefined
|
||||
}),
|
||||
).toEqual({
|
||||
type: "stream.view",
|
||||
view: { type: "prompt" },
|
||||
type: "question",
|
||||
request: other,
|
||||
})
|
||||
|
||||
ctrl.abort()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { questionToolRequestIndex } from "@/cli/cmd/tui/context/sync"
|
||||
|
||||
const request = {
|
||||
id: "question-new",
|
||||
sessionID: "session-1",
|
||||
questions: [],
|
||||
tool: { messageID: "msg-new", callID: "call-new" },
|
||||
} satisfies QuestionRequest
|
||||
|
||||
function part(status: "running" | "completed" | "error", tool = "question", callID = "call-new"): ToolPart {
|
||||
return {
|
||||
id: "part-new",
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-new",
|
||||
type: "tool",
|
||||
callID,
|
||||
tool,
|
||||
state:
|
||||
status === "running"
|
||||
? { status, input: {}, time: { start: 1 } }
|
||||
: status === "completed"
|
||||
? { status, input: {}, output: "", title: "question", metadata: {}, time: { start: 1, end: 2 } }
|
||||
: { status, input: {}, error: "Tool execution aborted", time: { start: 1, end: 2 } },
|
||||
}
|
||||
}
|
||||
|
||||
describe("tui sync", () => {
|
||||
test("matches terminal tool-owned question requests", () => {
|
||||
const stale = { ...request, id: "question-old", tool: { messageID: "msg-old", callID: "call-old" } }
|
||||
|
||||
expect(questionToolRequestIndex([stale, request], part("error"))).toBe(1)
|
||||
expect(questionToolRequestIndex([stale, request], part("completed"))).toBe(1)
|
||||
expect(questionToolRequestIndex([stale, request], part("completed", "plan_exit"))).toBe(1)
|
||||
expect(questionToolRequestIndex([stale, request], part("running"))).toBe(-1)
|
||||
expect(questionToolRequestIndex([stale, request], part("error", "bash", "call-other"))).toBe(-1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user