Skip to content

feat(mobile): show prompt in chat thread immediately on submit#2407

Merged
Gilbert09 merged 2 commits into
mainfrom
posthog-code/mobile-optimistic-prompt
May 28, 2026
Merged

feat(mobile): show prompt in chat thread immediately on submit#2407
Gilbert09 merged 2 commits into
mainfrom
posthog-code/mobile-optimistic-prompt

Conversation

@Gilbert09
Copy link
Copy Markdown
Member

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 pendingTaskPromptStore pattern:

  • pendingTaskPromptStore (new) — Zustand store with a byKey map and set/get/move/clear actions, plus a non-reactive pendingTaskPromptStoreApi for async flows. Pure UI state, not persisted.
  • NewTaskScreen — generates a transient UUID key on submit, writes the prompt to the store immediately, then moves it onto the real task id once createTask returns. Clears on failure.
  • TaskSessionView — accepts an optimisticUserMessage prop and appends it as the newest message, dedup'd against any matching user_message_chunk already in events. The stateful event processor is untouched.
  • TaskDetailScreen — reads the pending prompt for the current task id, passes it to TaskSessionView, 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.
  • Terminal-resume path (handleSendAfterTerminal) — also writes to the store so the user's text remains visible across the disconnect / runTaskInCloud / reconnect gap.

The in-session sendPrompt flow already had its own optimistic echo, so it's untouched.

Architecture notes (per CLAUDE.md)

  • Store is pure UI state with thin action wrappers — no business logic, no fetching, no multi-step flows.
  • No desktop code touched.
  • Existing imperative-API + reactive-hook pattern matches sibling stores (attachmentEchoStore, taskStore).

Test plan

  • Unit tests for pendingTaskPromptStore (set/get/move/clear/attachments)
  • TaskSessionView renders the optimistic echo when no SSE copy exists
  • TaskSessionView suppresses the optimistic echo once the real user_message_chunk arrives
  • Full mobile test suite passes (112 tests)
  • Biome clean on changed files
  • No new tsc errors on changed files (pre-existing test-file errors unchanged)
  • Manual: submit a new task in dev build, verify prompt appears instantly in thread
  • Manual: continue a terminal/completed task, verify prompt appears during the resume gap
  • Manual: submit a task with an image attachment, verify it shows in the optimistic bubble

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
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Prompt To Fix All With AI
Fix 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

Comment on lines +857 to +860
const alreadyEchoed = messages.some(
(m) => m.type === "user" && m.content === optimisticUserMessage.text,
);
if (alreadyEchoed) return messages;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +74 to +104
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);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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
@Gilbert09 Gilbert09 added the Stamphog This will request an autostamp by stamphog on small changes label May 28, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Gilbert09 Gilbert09 merged commit 32ebc98 into main May 28, 2026
20 checks passed
@Gilbert09 Gilbert09 deleted the posthog-code/mobile-optimistic-prompt branch May 28, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stamphog This will request an autostamp by stamphog on small changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant