fix(tui): clear stale tool preparation state
Prefer the terminal server projection when refresh observes a completed assistant message, so a missed tool-failure event cannot leave the TUI showing an uncancellable pending tool. Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
This commit is contained in:
parent
d0733f83bf
commit
ac2e4b2d96
2 changed files with 112 additions and 1 deletions
|
|
@ -832,7 +832,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
const messages = [
|
const messages = [
|
||||||
...loaded.map((message) => {
|
...loaded.map((message) => {
|
||||||
if (message.type === "user") return message
|
if (message.type === "user") return message
|
||||||
return liveByID.get(message.id) ?? message
|
const live = liveByID.get(message.id)
|
||||||
|
if (!live || ("completed" in message.time && message.time.completed !== undefined)) return message
|
||||||
|
return live
|
||||||
}),
|
}),
|
||||||
...live.filter((message) => !loadedIDs.has(message.id)),
|
...live.filter((message) => !loadedIDs.has(message.id)),
|
||||||
].toSorted((a, b) => a.time.created - b.time.created)
|
].toSorted((a, b) => a.time.created - b.time.created)
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,115 @@ test("restores running manual compaction before applying live deltas", async ()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("refresh replaces stale completed messages with the terminal server projection", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
const sessionID = "session-stale-tool"
|
||||||
|
const messageID = "message-stale-tool"
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname === `/api/session/${sessionID}/message`)
|
||||||
|
return json({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: messageID,
|
||||||
|
type: "assistant",
|
||||||
|
agent: "build",
|
||||||
|
model: { id: "model", providerID: "provider" },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool",
|
||||||
|
id: "call-write",
|
||||||
|
name: "write",
|
||||||
|
provider: { executed: false },
|
||||||
|
state: {
|
||||||
|
status: "error",
|
||||||
|
input: {},
|
||||||
|
content: [],
|
||||||
|
structured: {},
|
||||||
|
error: { type: "unknown", message: "Tool execution interrupted" },
|
||||||
|
},
|
||||||
|
time: { created: 1, completed: 3 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
finish: "error",
|
||||||
|
error: { type: "unknown", message: "Step interrupted" },
|
||||||
|
time: { created: 1, completed: 3 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cursor: {},
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
}, events)
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_step_started_stale_tool",
|
||||||
|
created: 1,
|
||||||
|
type: "session.step.started",
|
||||||
|
durable: durable(sessionID),
|
||||||
|
data: {
|
||||||
|
sessionID,
|
||||||
|
assistantMessageID: messageID,
|
||||||
|
agent: "build",
|
||||||
|
model: { id: "model", providerID: "provider" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_tool_started_stale_tool",
|
||||||
|
created: 2,
|
||||||
|
type: "session.tool.input.started",
|
||||||
|
durable: durable(sessionID, 1),
|
||||||
|
data: {
|
||||||
|
sessionID,
|
||||||
|
assistantMessageID: messageID,
|
||||||
|
callID: "call-write",
|
||||||
|
name: "write",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_step_failed_stale_tool",
|
||||||
|
created: 3,
|
||||||
|
type: "session.step.failed",
|
||||||
|
durable: durable(sessionID, 2),
|
||||||
|
data: {
|
||||||
|
sessionID,
|
||||||
|
assistantMessageID: messageID,
|
||||||
|
error: { type: "unknown", message: "Step interrupted" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await wait(() => {
|
||||||
|
const message = data.session.message.get(sessionID, messageID)
|
||||||
|
return message?.type === "assistant" && message.time.completed === 3
|
||||||
|
})
|
||||||
|
await data.session.message.refresh(sessionID)
|
||||||
|
|
||||||
|
const message = data.session.message.get(sessionID, messageID)
|
||||||
|
expect(message?.type).toBe("assistant")
|
||||||
|
if (message?.type !== "assistant") return
|
||||||
|
expect(message.content[0]).toMatchObject({ type: "tool", state: { status: "error" } })
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("reconnects the event stream and bootstraps fresh data", async () => {
|
test("reconnects the event stream and bootstraps fresh data", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const requests = { active: 0, event: 0, model: 0 }
|
const requests = { active: 0, event: 0, model: 0 }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue