diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts index 69ca494992b0..a245ddaa6cc9 100644 --- a/packages/app/src/context/global-sync/utils.test.ts +++ b/packages/app/src/context/global-sync/utils.test.ts @@ -53,7 +53,7 @@ describe("normalizePermissionRequest", () => { resources: ["README.md"], save: ["*.md"], metadata: { path: "README.md" }, - source: { type: "tool", messageID: "message-1", callID: "call-1" }, + source: { type: "tool", messageID: "message-1", id: "call-1" }, }), ).toEqual({ id: "permission-1", diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index 0fd40ff2dbe2..1d917d7da71d 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -48,7 +48,7 @@ export function normalizePermissionRequest(input: PermissionRequest | LegacyPerm always: input.save ?? [], metadata: input.metadata ?? {}, tool: - input.source?.type === "tool" ? { messageID: input.source.messageID, callID: input.source.callID } : undefined, + input.source?.type === "tool" ? { messageID: input.source.messageID, callID: input.source.id } : undefined, } } diff --git a/packages/app/src/context/server-sdk.test.ts b/packages/app/src/context/server-sdk.test.ts index a7df4fe6a864..b5966ce77f9a 100644 --- a/packages/app/src/context/server-sdk.test.ts +++ b/packages/app/src/context/server-sdk.test.ts @@ -21,12 +21,24 @@ describe("adaptServerEvent", () => { id: "evt_1", created: 1, type: "permission.asked", - data: { id: "perm_1", sessionID: "ses_1", action: "read", resources: ["src/**"] }, + data: { + id: "perm_1", + sessionID: "ses_1", + action: "read", + resources: ["src/**"], + source: { type: "tool", messageID: "msg_1", id: "call_1" }, + }, } as OpenCodeEvent expect(adaptServerEvent(current)).toMatchObject({ type: "permission.asked", - properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] }, + properties: { + id: "perm_1", + sessionID: "ses_1", + permission: "read", + patterns: ["src/**"], + tool: { messageID: "msg_1", callID: "call_1" }, + }, current, }) }) @@ -70,6 +82,26 @@ describe("coalesceServerEvents", () => { expect(result[0]?.payload.current).toMatchObject({ id: "evt_2", data: { delta: "hello world" } }) }) + test("coalesces current tool input deltas by tool ID", () => { + const current = (eventID: string, id: string, delta: string) => + adaptServerEvent({ + id: eventID, + created: 1, + type: "session.tool.input.delta", + location: { directory: "/repo" }, + data: { sessionID: "ses", assistantMessageID: "msg", id, delta }, + } as OpenCodeEvent) + const result = coalesceServerEvents([ + { directory: "/repo", payload: current("evt_1", "call_1", "{") }, + { directory: "/repo", payload: current("evt_2", "call_1", "}") }, + { directory: "/repo", payload: current("evt_3", "call_2", "[]") }, + ]) + + expect(result).toHaveLength(2) + expect(result[0]?.payload.current).toMatchObject({ id: "evt_2", data: { id: "call_1", delta: "{}" } }) + expect(result[1]?.payload.current).toMatchObject({ id: "evt_3", data: { id: "call_2", delta: "[]" } }) + }) + test("preserves event boundaries and distinct fields", () => { const status = { directory: "/repo", diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 8c6bd577b8ff..aaf43703ef2b 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -39,7 +39,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { metadata: event.data.metadata ?? {}, tool: event.data.source?.type === "tool" - ? { messageID: event.data.source.messageID, callID: event.data.source.callID } + ? { messageID: event.data.source.messageID, callID: event.data.source.id } : undefined, }, current: event, @@ -142,7 +142,7 @@ function currentDelta(event: OpenCodeEvent | undefined): CurrentDelta | undefine function currentDeltaKey(event: CurrentDelta) { if (event.type === "session.tool.input.delta") - return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.callID}` + return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.id}` if (event.type === "session.compaction.delta") return `${event.type}:${event.data.sessionID}` return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.ordinal}` } diff --git a/packages/app/src/context/server-session-v2-reducer.test.ts b/packages/app/src/context/server-session-v2-reducer.test.ts index 00cc37cf5240..96100d636759 100644 --- a/packages/app/src/context/server-session-v2-reducer.test.ts +++ b/packages/app/src/context/server-session-v2-reducer.test.ts @@ -92,19 +92,19 @@ describe("v2 session reducer", () => { ...base, id: "evt_tool_start", type: "session.tool.input.started", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", name: "bash" }, + data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", id: "call_1", name: "bash" }, }) apply({ ...base, id: "evt_tool_delta", type: "session.tool.input.delta", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", delta: "{}" }, + data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", id: "call_1", delta: "{}" }, }) apply({ ...base, id: "evt_tool_called", type: "session.tool.called", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", input: {}, executed: true }, + data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", id: "call_1", input: {}, executed: true }, }) apply({ ...base, @@ -113,7 +113,7 @@ describe("v2 session reducer", () => { data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", - callID: "call_1", + id: "call_1", metadata: {}, content: [{ type: "text", text: "done" }], executed: true, diff --git a/packages/app/src/context/server-session-v2-reducer.ts b/packages/app/src/context/server-session-v2-reducer.ts index b34ab3985ec1..12f792d2125e 100644 --- a/packages/app/src/context/server-session-v2-reducer.ts +++ b/packages/app/src/context/server-session-v2-reducer.ts @@ -241,13 +241,13 @@ export function createV2SessionReducer() { case "session.tool.input.started": return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ ...item, - content: item.content.some((content) => content.type === "tool" && content.id === event.data.callID) + content: item.content.some((content) => content.type === "tool" && content.id === event.data.id) ? item.content : [ ...item.content, { type: "tool", - id: event.data.callID, + id: event.data.id, name: event.data.name, state: { status: "streaming", input: "" }, time: { created: event.created }, @@ -255,17 +255,17 @@ export function createV2SessionReducer() { ], })) case "session.tool.input.delta": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => + return updateTool(source, event.data.assistantMessageID, event.data.id, sessionID, (tool) => tool.state.status === "streaming" ? { ...tool, state: { ...tool.state, input: tool.state.input + event.data.delta } } : tool, ) case "session.tool.input.ended": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => + return updateTool(source, event.data.assistantMessageID, event.data.id, sessionID, (tool) => tool.state.status === "streaming" ? { ...tool, state: { ...tool.state, input: event.data.text } } : tool, ) case "session.tool.called": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => ({ + return updateTool(source, event.data.assistantMessageID, event.data.id, sessionID, (tool) => ({ ...tool, executed: event.data.executed, providerState: event.data.state, @@ -274,7 +274,7 @@ export function createV2SessionReducer() { time: { ...tool.time, ran: event.created }, })) case "session.tool.progress": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => + return updateTool(source, event.data.assistantMessageID, event.data.id, sessionID, (tool) => tool.state.status === "running" ? { ...tool, @@ -284,7 +284,7 @@ export function createV2SessionReducer() { : tool, ) case "session.tool.success": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => { + return updateTool(source, event.data.assistantMessageID, event.data.id, sessionID, (tool) => { if (tool.state.status !== "running") return tool return { ...tool, @@ -302,7 +302,7 @@ export function createV2SessionReducer() { } }) case "session.tool.failed": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => { + return updateTool(source, event.data.assistantMessageID, event.data.id, sessionID, (tool) => { if (tool.state.status !== "streaming" && tool.state.status !== "running") return tool return { ...tool,