Skip to content

Commit 427da82

Browse files
authored
fix(agent): preserve messages until send succeeds
1 parent 0092213 commit 427da82

3 files changed

Lines changed: 46 additions & 18 deletions

File tree

packages/ui/src/features/sessions/components/SessionView.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Pause, Spinner, Warning } from "@phosphor-icons/react";
2+
import { contentToXml } from "@posthog/core/message-editor/content";
23
import {
34
createLatestPlanTracker,
45
SESSION_SERVICE,
@@ -68,7 +69,7 @@ interface SessionViewProps {
6869
isPromptPending?: boolean | null;
6970
promptStartedAt?: number | null;
7071
onBeforeSubmit?: (text: string, clearEditor: () => void) => boolean;
71-
onSendPrompt: (text: string) => void;
72+
onSendPrompt: (text: string) => Promise<boolean>;
7273
onBashCommand?: (command: string) => void;
7374
onCancelPrompt: () => void;
7475
repoPath?: string | null;
@@ -299,6 +300,9 @@ export function SessionView({
299300
]);
300301

301302
const isCloudRun = useIsWorkspaceCloudRun(taskId);
303+
const editorRef = useRef<PromptInputHandle>(null);
304+
const sendInFlightRef = useRef(false);
305+
const [isSendingPrompt, setIsSendingPrompt] = useState(false);
302306

303307
const latestPlanTrackerRef = useRef<ReturnType<
304308
typeof createLatestPlanTracker
@@ -309,11 +313,22 @@ export function SessionView({
309313
(): Plan | null => latestPlanTracker.update(events) as Plan | null,
310314
[events, latestPlanTracker],
311315
);
312-
313316
const handleSubmit = useCallback(
314-
(text: string) => {
315-
if (text.trim()) {
316-
onSendPrompt(text);
317+
async (text: string): Promise<void> => {
318+
if (!text.trim() || sendInFlightRef.current) return;
319+
320+
sendInFlightRef.current = true;
321+
setIsSendingPrompt(true);
322+
try {
323+
if (await onSendPrompt(text)) {
324+
const editor = editorRef.current;
325+
if (editor && contentToXml(editor.getContent()) === text) {
326+
editor.clear();
327+
}
328+
}
329+
} finally {
330+
sendInFlightRef.current = false;
331+
setIsSendingPrompt(false);
317332
}
318333
},
319334
[onSendPrompt],
@@ -337,7 +352,6 @@ export function SessionView({
337352
const cancelQueuedEdit = useCancelQueuedMessageEdit(taskId);
338353

339354
const [isDraggingFile, setIsDraggingFile] = useState(false);
340-
const editorRef = useRef<PromptInputHandle>(null);
341355
const promptRecallRef = useRef<PromptRecallHandler | null>(null);
342356
const handlePromptRecall = useCallback<PromptRecallHandler>(
343357
(direction) => promptRecallRef.current?.(direction) ?? null,
@@ -678,8 +692,9 @@ export function SessionView({
678692
placeholder="Type a message... @ to mention files, ! for bash mode, / for skills"
679693
disabled={!isRunning && !handoffInProgress}
680694
submitDisabledExternal={
681-
handoffInProgress || !isOnline
695+
handoffInProgress || !isOnline || isSendingPrompt
682696
}
697+
clearOnSubmit={false}
683698
submitTooltipOverride={
684699
!isOnline ? "No internet connection" : undefined
685700
}

packages/ui/src/features/sessions/hooks/useSessionCallbacks.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ describe("useSessionCallbacks.handleSendPrompt while editing a queued message",
163163
});
164164
});
165165

166+
describe("useSessionCallbacks.handleSendPrompt", () => {
167+
beforeEach(() => {
168+
vi.clearAllMocks();
169+
sessionState.editingQueuedId = undefined;
170+
sessionState.messageQueue = [];
171+
});
172+
173+
it("reports a failed send so the composer keeps its content", async () => {
174+
sessionService.sendPrompt.mockRejectedValue(new Error("fetch failed"));
175+
176+
const { result } = renderCallbacks();
177+
const sent = await result.current.handleSendPrompt("keep this message");
178+
179+
expect(sent).toBe(false);
180+
expect(toastError).toHaveBeenCalledWith("fetch failed");
181+
});
182+
});
183+
166184
describe("useSessionCallbacks.handleCancelPrompt", () => {
167185
beforeEach(() => {
168186
vi.clearAllMocks();

packages/ui/src/features/sessions/hooks/useSessionCallbacks.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function useSessionCallbacks({
6262
const messagingMode = useMessagingMode(taskId);
6363

6464
const handleSendPrompt = useCallback(
65-
async (text: string) => {
65+
async (text: string): Promise<boolean> => {
6666
const currentSession = sessionRef.current;
6767
const currentEvents = currentSession?.events ?? [];
6868
const handled = await tryExecuteCodeCommand(text, {
@@ -77,7 +77,7 @@ export function useSessionCallbacks({
7777
: null,
7878
taskRun: task.latest_run ?? null,
7979
});
80-
if (handled) return;
80+
if (handled) return true;
8181

8282
let promptText =
8383
rewriteLocalSkillCommandPrompt(
@@ -109,7 +109,7 @@ export function useSessionCallbacks({
109109
);
110110
if (updated) {
111111
markAsViewed(taskId);
112-
return;
112+
return true;
113113
}
114114
// Target no longer queued — drop the stale hold and send as new.
115115
sessionService.clearEditingQueuedMessage(taskId);
@@ -127,7 +127,7 @@ export function useSessionCallbacks({
127127
setPendingContent(taskId, xmlToContent(promptText ?? text));
128128
requestFocus(taskId);
129129
}
130-
return;
130+
return false;
131131
}
132132
}
133133

@@ -144,18 +144,13 @@ export function useSessionCallbacks({
144144
if (isViewingTask) {
145145
markAsViewed(taskId);
146146
}
147+
return true;
147148
} catch (error) {
148149
const message =
149150
error instanceof Error ? error.message : "Failed to send message";
150-
// The composer clears optimistically on submit, so a failed send
151-
// would otherwise lose the typed prompt. Restore it — unless the
152-
// user has already started typing a new message.
153-
if (isContentEmpty(useDraftStore.getState().drafts[taskId] ?? null)) {
154-
setPendingContent(taskId, xmlToContent(text));
155-
requestFocus(taskId);
156-
}
157151
toast.error(message);
158152
log.error("Failed to send prompt", error);
153+
return false;
159154
}
160155
},
161156
[

0 commit comments

Comments
 (0)