mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
fix(opencode): hide user messages OpenCode marks synthetic (#5434)
OpenCode adds text to user messages on its own behalf (continue prompts after compaction or a subtask, MCP resource contents, plugin prompts) and flags that text `synthetic: true`. Its own UI hides it. Paseo built user bubbles from every text part, so reopening an OpenCode chat showed those messages as if the user had typed them, and appended synthetic text to the user's own message. Closes #5425 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1944,6 +1944,75 @@ describe("OpenCode adapter startTurn error handling", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("streamHistory hides user text that OpenCode marks synthetic", async () => {
|
||||
const fakeClient = {
|
||||
session: {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: { revert: undefined },
|
||||
error: undefined,
|
||||
}),
|
||||
messages: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
info: { id: "msg_continue", sessionID: "ses_unit_test", role: "user" },
|
||||
parts: [
|
||||
{
|
||||
id: "prt_continue",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_continue",
|
||||
type: "text",
|
||||
text: "Summarize the task tool output above and continue with your task.",
|
||||
synthetic: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
info: { id: "msg_user", sessionID: "ses_unit_test", role: "user" },
|
||||
parts: [
|
||||
{
|
||||
id: "prt_user",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_user",
|
||||
type: "text",
|
||||
text: "Read the notes",
|
||||
},
|
||||
{
|
||||
id: "prt_resource",
|
||||
sessionID: "ses_unit_test",
|
||||
messageID: "msg_user",
|
||||
type: "text",
|
||||
text: "Reading MCP resource: notes.md",
|
||||
synthetic: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
error: undefined,
|
||||
}),
|
||||
},
|
||||
} as never;
|
||||
|
||||
const session = new __openCodeInternals.OpenCodeAgentSession(
|
||||
{ provider: "opencode", cwd: "/tmp/test" },
|
||||
fakeClient,
|
||||
"ses_unit_test",
|
||||
createTestLogger(),
|
||||
);
|
||||
|
||||
const history: AgentStreamEvent[] = [];
|
||||
for await (const event of session.streamHistory()) {
|
||||
history.push(event);
|
||||
}
|
||||
|
||||
expect(history).toEqual([
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: { type: "user_message", text: "Read the notes", messageId: "msg_user" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("streamHistory omits replay timestamps when OpenCode omits times", async () => {
|
||||
const fakeClient = {
|
||||
session: {
|
||||
|
||||
@@ -1318,7 +1318,10 @@ function buildOpenCodeReplayTimelineEvents(
|
||||
}
|
||||
if (info.role === "user") {
|
||||
const text = parts
|
||||
.filter((part): part is Extract<OpenCodePart, { type: "text" }> => part.type === "text")
|
||||
.filter(
|
||||
(part): part is Extract<OpenCodePart, { type: "text" }> =>
|
||||
part.type === "text" && isUserAuthoredOpenCodeText(part),
|
||||
)
|
||||
.map((part) => part.text)
|
||||
.join("");
|
||||
|
||||
@@ -2854,6 +2857,11 @@ function shouldSuppressOpenCodeAssistantPart(
|
||||
);
|
||||
}
|
||||
|
||||
// OpenCode marks text it adds to a user message itself with `synthetic` and hides it from its own UI.
|
||||
function isUserAuthoredOpenCodeText(part: { synthetic?: boolean }): boolean {
|
||||
return part.synthetic !== true;
|
||||
}
|
||||
|
||||
function appendOpenCodeTextPart(
|
||||
part: Extract<
|
||||
Extract<OpenCodeEvent, { type: "message.part.updated" }>["properties"]["part"],
|
||||
@@ -2864,7 +2872,11 @@ function appendOpenCodeTextPart(
|
||||
events: AgentStreamEvent[],
|
||||
): void {
|
||||
if (messageRole === "user") {
|
||||
if (!part.text || state.emittedUserMessageIds?.has(part.messageID)) {
|
||||
if (
|
||||
!part.text ||
|
||||
!isUserAuthoredOpenCodeText(part) ||
|
||||
state.emittedUserMessageIds?.has(part.messageID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
state.emittedUserMessageIds?.add(part.messageID);
|
||||
|
||||
@@ -1276,6 +1276,44 @@ describe("translateOpenCodeEvent", () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("shows OpenCode user text parts unless OpenCode marks them synthetic", () => {
|
||||
const state = createState();
|
||||
const userTextPart = (messageId: string, text: string, synthetic: boolean) => {
|
||||
translateOpenCodeEvent(
|
||||
{
|
||||
type: "message.updated",
|
||||
properties: { info: { id: messageId, sessionID: "session-1", role: "user" } },
|
||||
},
|
||||
state,
|
||||
);
|
||||
return translateOpenCodeEvent(
|
||||
{
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
part: {
|
||||
id: `part-${messageId}`,
|
||||
sessionID: "session-1",
|
||||
messageID: messageId,
|
||||
type: "text",
|
||||
text,
|
||||
synthetic,
|
||||
},
|
||||
},
|
||||
},
|
||||
state,
|
||||
);
|
||||
};
|
||||
|
||||
expect(userTextPart("msg-continue", "Continue if you have next steps", true)).toEqual([]);
|
||||
expect(userTextPart("msg-user", "Plugin prompt", false)).toEqual([
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "opencode",
|
||||
item: { type: "user_message", text: "Plugin prompt", messageId: "msg-user" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores message.part.delta for user messages", () => {
|
||||
const state = createState();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user