feat(mobile): show prompt in chat thread immediately on submit#2407
Conversation
Ports desktop PR #2310 to the React Native app. The user's submitted prompt now appears in the chat thread the instant they tap send, instead of waiting for the cloud run to start and the SSE stream to deliver the canonical echo. Mechanism mirrors the desktop one: - A keyed `pendingTaskPromptStore` (Zustand) holds the optimistic prompt. `NewTaskScreen` sets it under a transient UUID the moment the user submits, then `move`s it onto the real task id as soon as `createTask` returns. - `TaskSessionView` accepts an `optimisticUserMessage` prop and appends it as the newest message, dedup'd against any matching real `user_message_chunk` that has already arrived. - `TaskDetailScreen` reads the pending prompt for the current task id and clears it once the live session echoes the same text back. - The full-screen loading overlay is suppressed while an optimistic prompt is showing — the user sees their own text plus the connecting/thinking strip instead of a blank spinner. - The terminal-resume path (`handleSendAfterTerminal`) also writes to the store so the user's text remains visible across the disconnect/reconnect gap. Tests: new unit tests for the store (set/get/move/clear/attachments) and two new `TaskSessionView` rendering tests for the optimistic-echo path (renders when no SSE echo, suppressed when one matches). Generated-By: PostHog Code Task-Id: d59fa139-b382-4fcc-b6fd-9282bbc406e2
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/mobile/src/features/tasks/components/TaskSessionView.tsx:857-860
**Dedup ignores attachments, matching on text alone**
`alreadyEchoed` checks `m.content === optimisticUserMessage.text` but never compares attachments. For a message that is attachment-only (empty `text`) or a session that already contains a previous user turn with the same text, the condition fires immediately and the optimistic echo is dropped before the real SSE copy arrives. In practice this mostly matters for attachment-only submissions (`content === ""` on both sides) if more than one such message exists in `messages`, but the logic gap means any text-identical historical user message would also suppress the optimistic echo incorrectly.
### Issue 2 of 2
apps/mobile/src/features/tasks/components/TaskSessionView.test.tsx:74-104
**Prefer a parameterised test for the optimistic-echo cases**
Both new tests exercise the same shape — a fixed `optimisticUserMessage`, varying `events`, and then asserting `HumanMessage` count (and optionally content). This is a natural `it.each` table: one row for "no SSE echo → 1 message with the right content", another for "matching SSE chunk → 1 message (deduped)". Collapsing them into a single parameterised test reduces boilerplate and makes it easier to add edge-cases (e.g. non-matching SSE text, attachment-only prompt) without duplicating the render/find harness.
Reviews (1): Last reviewed commit: "feat(mobile): show prompt in chat thread..." | Re-trigger Greptile |
| const alreadyEchoed = messages.some( | ||
| (m) => m.type === "user" && m.content === optimisticUserMessage.text, | ||
| ); | ||
| if (alreadyEchoed) return messages; |
There was a problem hiding this comment.
Dedup ignores attachments, matching on text alone
alreadyEchoed checks m.content === optimisticUserMessage.text but never compares attachments. For a message that is attachment-only (empty text) or a session that already contains a previous user turn with the same text, the condition fires immediately and the optimistic echo is dropped before the real SSE copy arrives. In practice this mostly matters for attachment-only submissions (content === "" on both sides) if more than one such message exists in messages, but the logic gap means any text-identical historical user message would also suppress the optimistic echo incorrectly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/features/tasks/components/TaskSessionView.tsx
Line: 857-860
Comment:
**Dedup ignores attachments, matching on text alone**
`alreadyEchoed` checks `m.content === optimisticUserMessage.text` but never compares attachments. For a message that is attachment-only (empty `text`) or a session that already contains a previous user turn with the same text, the condition fires immediately and the optimistic echo is dropped before the real SSE copy arrives. In practice this mostly matters for attachment-only submissions (`content === ""` on both sides) if more than one such message exists in `messages`, but the logic gap means any text-identical historical user message would also suppress the optimistic echo incorrectly.
How can I resolve this? If you propose a fix, please make it concise.| it("renders the optimistic user message when no SSE echo has arrived", () => { | ||
| const renderer = renderTaskSessionView({ | ||
| events: [], | ||
| optimisticUserMessage: { text: "Ship it" }, | ||
| }); | ||
|
|
||
| const humans = findHumanMessages(renderer); | ||
| expect(humans).toHaveLength(1); | ||
| expect(humans[0].props.content).toBe("Ship it"); | ||
| }); | ||
|
|
||
| it("suppresses the optimistic echo once the real user message lands", () => { | ||
| const renderer = renderTaskSessionView({ | ||
| events: [ | ||
| { | ||
| type: "session_update" as const, | ||
| ts: 1, | ||
| notification: { | ||
| update: { | ||
| sessionUpdate: "user_message_chunk", | ||
| content: { type: "text", text: "Ship it" }, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| optimisticUserMessage: { text: "Ship it" }, | ||
| }); | ||
|
|
||
| const humans = findHumanMessages(renderer); | ||
| expect(humans).toHaveLength(1); | ||
| }); |
There was a problem hiding this comment.
Prefer a parameterised test for the optimistic-echo cases
Both new tests exercise the same shape — a fixed optimisticUserMessage, varying events, and then asserting HumanMessage count (and optionally content). This is a natural it.each table: one row for "no SSE echo → 1 message with the right content", another for "matching SSE chunk → 1 message (deduped)". Collapsing them into a single parameterised test reduces boilerplate and makes it easier to add edge-cases (e.g. non-matching SSE text, attachment-only prompt) without duplicating the render/find harness.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/features/tasks/components/TaskSessionView.test.tsx
Line: 74-104
Comment:
**Prefer a parameterised test for the optimistic-echo cases**
Both new tests exercise the same shape — a fixed `optimisticUserMessage`, varying `events`, and then asserting `HumanMessage` count (and optionally content). This is a natural `it.each` table: one row for "no SSE echo → 1 message with the right content", another for "matching SSE chunk → 1 message (deduped)". Collapsing them into a single parameterised test reduces boilerplate and makes it easier to add edge-cases (e.g. non-matching SSE text, attachment-only prompt) without duplicating the render/find harness.
**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Addresses Greptile review feedback on the dedup logic.
Previously the optimistic-echo dedup matched against any historical
user message with identical text, so resubmitting the same prompt
("Continue", "Continue", ...) would suppress the new optimistic
bubble immediately — the prior message was treated as the canonical
echo for the new submission.
Adds a `setAt: number` submit-time timestamp to `PendingTaskPrompt`
and gates both the in-view dedup (TaskSessionView) and the
store-clear effect (TaskDetailScreen) on `event.ts >= setAt`. A
text-identical historical turn now correctly leaves the new optimistic
echo in place until its own canonical SSE copy arrives.
Tests: collapses the two prior optimistic-echo cases into a single
`it.each` table and adds two edge cases:
- text-identical historical turn → optimistic still renders
- non-matching SSE text → optimistic still renders
Generated-By: PostHog Code
Task-Id: d59fa139-b382-4fcc-b6fd-9282bbc406e2
There was a problem hiding this comment.
Well-contained mobile-only feature addition with proper tests — new Zustand store, view-layer dedup logic, and parameterised tests covering the key edge cases. The bot's test-style concern was addressed (it.each is used). The remaining attachment-dedup edge case (empty-text messages matching too eagerly) is a P2 UX issue, not a crash, data loss, or security risk.
Summary
Ports desktop PR #2310 to the React Native app: the user's submitted prompt now appears in the chat thread the moment they tap send, instead of waiting for the cloud run to start and the SSE stream to deliver the canonical echo.
Mechanism
Mirrors desktop's keyed
pendingTaskPromptStorepattern:pendingTaskPromptStore(new) — Zustand store with abyKeymap andset/get/move/clearactions, plus a non-reactivependingTaskPromptStoreApifor async flows. Pure UI state, not persisted.NewTaskScreen— generates a transient UUID key on submit, writes the prompt to the store immediately, thenmoves it onto the real task id oncecreateTaskreturns. Clears on failure.TaskSessionView— accepts anoptimisticUserMessageprop and appends it as the newest message, dedup'd against any matchinguser_message_chunkalready inevents. The stateful event processor is untouched.TaskDetailScreen— reads the pending prompt for the current task id, passes it toTaskSessionView, and clears the store once the canonical SSE copy lands. Suppresses the full-screen loading overlay while the optimistic prompt is showing so the user sees their own text plus the existing connecting/thinking strip instead of a blank spinner.handleSendAfterTerminal) — also writes to the store so the user's text remains visible across the disconnect /runTaskInCloud/ reconnect gap.The in-session
sendPromptflow already had its own optimistic echo, so it's untouched.Architecture notes (per CLAUDE.md)
attachmentEchoStore,taskStore).Test plan
pendingTaskPromptStore(set/get/move/clear/attachments)TaskSessionViewrenders the optimistic echo when no SSE copy existsTaskSessionViewsuppresses the optimistic echo once the realuser_message_chunkarrives