Skip to content

Commit ffa7e41

Browse files
Switch to sync prompt endpoint for reliable message delivery
Use the synchronous /message endpoint instead of /prompt_async to ensure the UI receives authoritative response data. This matches OpenCode TUI behavior and prevents message desync during sub-agent tasks.
1 parent d78bb12 commit ffa7e41

File tree

3 files changed

+44
-10
lines changed

3 files changed

+44
-10
lines changed

frontend/src/api/opencode.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type CommandRequest = NonNullable<paths['/session/{sessionID}/command']['post'][
1212
type ShellRequest = NonNullable<paths['/session/{sessionID}/shell']['post']['requestBody']>['content']['application/json']
1313
type AgentListResponse = paths['/agent']['get']['responses']['200']['content']['application/json']
1414
type QuestionListResponse = paths['/question']['get']['responses']['200']['content']['application/json']
15+
type SendPromptResponse = paths['/session/{sessionID}/message']['post']['responses']['200']['content']['application/json']
16+
17+
export type { SendPromptResponse }
1518

1619
export class OpenCodeClient {
1720
private client: AxiosInstance
@@ -78,8 +81,13 @@ export class OpenCodeClient {
7881
return response.data
7982
}
8083

81-
async sendPrompt(sessionID: string, data: SendPromptRequest): Promise<void> {
82-
await this.client.post(`/session/${sessionID}/prompt_async`, data)
84+
async sendPrompt(sessionID: string, data: SendPromptRequest): Promise<SendPromptResponse> {
85+
const response = await this.client.post<SendPromptResponse>(
86+
`/session/${sessionID}/message`,
87+
data,
88+
{ timeout: 0 }
89+
)
90+
return response.data
8391
}
8492

8593
async summarizeSession(sessionID: string, providerID: string, modelID: string) {

frontend/src/hooks/useOpenCode.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -328,15 +328,26 @@ export const useSendPrompt = (opcodeUrl: string | null | undefined, directory?:
328328
requestData.variant = variant;
329329
}
330330

331-
await client.sendPrompt(sessionID, requestData);
331+
const response = await client.sendPrompt(sessionID, requestData);
332332

333-
return { optimisticUserID, userPromptText };
333+
return { optimisticUserID, userPromptText, response };
334334
},
335335
onError: (error, variables) => {
336336
const { sessionID } = variables;
337+
const messagesQueryKey = ["opencode", "messages", opcodeUrl, sessionID, directory];
338+
339+
const axiosError = error as { code?: string; response?: unknown };
340+
const isNetworkError = axiosError.code === 'ECONNABORTED' ||
341+
axiosError.code === 'ERR_NETWORK' ||
342+
!axiosError.response;
343+
344+
if (isNetworkError) {
345+
return;
346+
}
347+
337348
setSessionStatus(sessionID, { type: "idle" });
338349
queryClient.setQueryData<MessageListResponse>(
339-
["opencode", "messages", opcodeUrl, sessionID, directory],
350+
messagesQueryKey,
340351
(old) => old?.filter((msg) => !msg.info.id.startsWith("optimistic_")),
341352
);
342353

@@ -348,16 +359,28 @@ export const useSendPrompt = (opcodeUrl: string | null | undefined, directory?:
348359
},
349360
onSuccess: async (data, variables) => {
350361
const { sessionID } = variables;
351-
const { optimisticUserID, userPromptText } = data;
362+
const { optimisticUserID, userPromptText, response } = data;
363+
const messagesQueryKey = ["opencode", "messages", opcodeUrl, sessionID, directory];
352364

353365
queryClient.setQueryData<MessageListResponse>(
354-
["opencode", "messages", opcodeUrl, sessionID, directory],
366+
messagesQueryKey,
355367
(old) => {
356368
if (!old) return old;
357-
return old.filter((msg) => msg.info.id !== optimisticUserID);
369+
const withoutOptimistic = old.filter((msg) => msg.info.id !== optimisticUserID);
370+
371+
const existingIdx = withoutOptimistic.findIndex(m => m.info.id === response.info.id);
372+
if (existingIdx >= 0) {
373+
const updated = [...withoutOptimistic];
374+
updated[existingIdx] = { info: response.info, parts: response.parts };
375+
return updated;
376+
}
377+
378+
return [...withoutOptimistic, { info: response.info, parts: response.parts }];
358379
},
359380
);
360381

382+
setSessionStatus(sessionID, { type: "idle" });
383+
361384
queryClient.invalidateQueries({
362385
queryKey: ["opencode", "session", opcodeUrl, sessionID, directory],
363386
});

frontend/src/hooks/useSSE.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ export const useSSE = (opcodeUrl: string | null | undefined, directory?: string,
214214

215215
setSessionStatus(sessionID, { type: 'idle' })
216216

217-
const currentData = queryClient.getQueryData<MessageListResponse>(['opencode', 'messages', opcodeUrl, sessionID, directory])
217+
const messagesQueryKey = ['opencode', 'messages', opcodeUrl, sessionID, directory]
218+
const currentData = queryClient.getQueryData<MessageListResponse>(messagesQueryKey)
218219
if (!currentData) break
219220

220221
const now = Date.now()
@@ -253,7 +254,9 @@ export const useSSE = (opcodeUrl: string | null | undefined, directory?: string,
253254
}
254255
})
255256

256-
queryClient.setQueryData(['opencode', 'messages', opcodeUrl, sessionID, directory], updated)
257+
queryClient.setQueryData(messagesQueryKey, updated)
258+
259+
queryClient.invalidateQueries({ queryKey: messagesQueryKey })
257260
break
258261
}
259262

0 commit comments

Comments
 (0)