mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
cli: recover pending questions from plan_exit tool
The question recovery path only matched the "question" tool, so plan_exit prompts would get stuck when the question.asked event was missed during reconnection. Extend recovery to match any tool-owned question request and drop the tool-name guard in syncQuestion so completions clear the footer correctly.
This commit is contained in:
@@ -360,14 +360,10 @@ function syncPermission(data: SessionData, part: ToolPart): FooterOutput | undef
|
||||
}
|
||||
}
|
||||
|
||||
// Question tool replies can complete without a matching question.replied event.
|
||||
// When that happens, drop the recovered pending request tied to this tool call so
|
||||
// the footer can return to the next blocker or to the prompt.
|
||||
// Tool-owned question requests can complete without a matching question.replied
|
||||
// event. When that happens, drop the recovered pending request tied to this tool
|
||||
// call so the footer can return to the next blocker or to the prompt.
|
||||
function syncQuestion(data: SessionData, part: ToolPart): FooterOutput | undefined {
|
||||
if (part.tool !== "question") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (part.state.status !== "completed" && part.state.status !== "error") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -791,7 +791,7 @@ function createLayer(input: StreamInput) {
|
||||
event.type === "message.part.updated" &&
|
||||
event.properties.part.sessionID === input.sessionID &&
|
||||
event.properties.part.type === "tool" &&
|
||||
event.properties.part.tool === "question" &&
|
||||
(event.properties.part.tool === "question" || event.properties.part.tool === "plan_exit") &&
|
||||
event.properties.part.state.status === "running"
|
||||
) {
|
||||
yield* recoverQuestion(event.properties.part).pipe(
|
||||
|
||||
@@ -845,7 +845,7 @@ describe("run stream transport", () => {
|
||||
stream: src.stream,
|
||||
questions: async () => {
|
||||
questionCalls += 1
|
||||
return ok(questionCalls === 1 ? [other] : [other, request])
|
||||
return ok(questionCalls === 1 ? [other] : [request])
|
||||
},
|
||||
promptAsync: async () => {
|
||||
queueMicrotask(() => {
|
||||
@@ -908,6 +908,7 @@ describe("run stream transport", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const count = ui.events.length
|
||||
src.push(
|
||||
toolUpdated(
|
||||
completedTool({
|
||||
@@ -930,13 +931,126 @@ 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 === "question" && item.view.request.id === other.id
|
||||
? item.view
|
||||
: undefined
|
||||
return item?.type === "stream.view" && item.view.type === "prompt" ? item : undefined
|
||||
}),
|
||||
).toEqual({
|
||||
type: "stream.view",
|
||||
view: { type: "prompt" },
|
||||
})
|
||||
|
||||
expect(
|
||||
ui.events.slice(count).findLast(
|
||||
(event) => event.type === "stream.view" && event.view.type === "question" && event.view.request.id === other.id,
|
||||
),
|
||||
).toBeUndefined()
|
||||
|
||||
ctrl.abort()
|
||||
await run
|
||||
} finally {
|
||||
src.close()
|
||||
await transport.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("recovers pending plan_exit questions from question.list when question.asked is missed", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
let questionCalls = 0
|
||||
const request = {
|
||||
id: "question-plan-1",
|
||||
sessionID: "session-1",
|
||||
questions: [
|
||||
{
|
||||
question: "Plan is complete. Start implementing it now?",
|
||||
header: "Build Agent",
|
||||
options: [{ label: "Yes", description: "Switch to build agent and start implementing." }],
|
||||
multiple: false,
|
||||
},
|
||||
],
|
||||
tool: {
|
||||
messageID: "msg-plan-1",
|
||||
callID: "call-plan-exit-1",
|
||||
},
|
||||
}
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
questions: async () => {
|
||||
questionCalls += 1
|
||||
return ok(questionCalls === 1 ? [] : [request])
|
||||
},
|
||||
promptAsync: async () => {
|
||||
queueMicrotask(() => {
|
||||
src.push(busy())
|
||||
src.push(assistant("msg-plan-1"))
|
||||
src.push(
|
||||
toolUpdated(
|
||||
runningTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-plan-1",
|
||||
id: "plan-exit-tool-1",
|
||||
callID: "call-plan-exit-1",
|
||||
tool: "plan_exit",
|
||||
body: {},
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
return ok(undefined)
|
||||
},
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
const ctrl = new AbortController()
|
||||
|
||||
try {
|
||||
const run = transport.runPromptTurn({
|
||||
agent: undefined,
|
||||
model: undefined,
|
||||
variant: undefined,
|
||||
prompt: { text: "hello", parts: [] },
|
||||
files: [],
|
||||
includeFiles: false,
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
|
||||
expect(
|
||||
await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.view")
|
||||
return item?.type === "stream.view" && item.view.type === "question" ? item.view : undefined
|
||||
}),
|
||||
).toEqual({
|
||||
type: "question",
|
||||
request: other,
|
||||
request,
|
||||
})
|
||||
|
||||
src.push(
|
||||
toolUpdated(
|
||||
completedTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-plan-1",
|
||||
id: "plan-exit-tool-1",
|
||||
callID: "call-plan-exit-1",
|
||||
tool: "plan_exit",
|
||||
body: {},
|
||||
output: "User approved switching to build agent.",
|
||||
metadata: {},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(
|
||||
await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.view")
|
||||
return item?.type === "stream.view" && item.view.type === "prompt" ? item : undefined
|
||||
}),
|
||||
).toEqual({
|
||||
type: "stream.view",
|
||||
view: { type: "prompt" },
|
||||
})
|
||||
|
||||
ctrl.abort()
|
||||
|
||||
Reference in New Issue
Block a user