Skip to content

Commit 9dbd327

Browse files
authored
fix(chat): preserve messages until send succeeds
Generated-By: PostHog Code Task-Id: a0ce6802-e1c9-4c53-bcf0-85bacfdf2536
1 parent 0092213 commit 9dbd327

3 files changed

Lines changed: 39 additions & 17 deletions

File tree

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ interface SessionViewProps {
6868
isPromptPending?: boolean | null;
6969
promptStartedAt?: number | null;
7070
onBeforeSubmit?: (text: string, clearEditor: () => void) => boolean;
71-
onSendPrompt: (text: string) => void;
71+
onSendPrompt: (text: string) => Promise<boolean>;
7272
onBashCommand?: (command: string) => void;
7373
onCancelPrompt: () => void;
7474
repoPath?: string | null;
@@ -309,14 +309,22 @@ export function SessionView({
309309
(): Plan | null => latestPlanTracker.update(events) as Plan | null,
310310
[events, latestPlanTracker],
311311
);
312+
const [isSendingPrompt, setIsSendingPrompt] = useState(false);
312313

313314
const handleSubmit = useCallback(
314-
(text: string) => {
315-
if (text.trim()) {
316-
onSendPrompt(text);
315+
async (text: string): Promise<void> => {
316+
if (!text.trim() || isSendingPrompt) return;
317+
318+
setIsSendingPrompt(true);
319+
try {
320+
if (await onSendPrompt(text)) {
321+
editorRef.current?.clear();
322+
}
323+
} finally {
324+
setIsSendingPrompt(false);
317325
}
318326
},
319-
[onSendPrompt],
327+
[isSendingPrompt, onSendPrompt],
320328
);
321329

322330
const handleBeforeSubmit = useCallback(
@@ -678,8 +686,9 @@ export function SessionView({
678686
placeholder="Type a message... @ to mention files, ! for bash mode, / for skills"
679687
disabled={!isRunning && !handoffInProgress}
680688
submitDisabledExternal={
681-
handoffInProgress || !isOnline
689+
handoffInProgress || !isOnline || isSendingPrompt
682690
}
691+
clearOnSubmit={false}
683692
submitTooltipOverride={
684693
!isOnline ? "No internet connection" : undefined
685694
}

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)