diff --git a/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx b/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx
index b02c4e2..a6472eb 100644
--- a/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx
+++ b/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx
@@ -933,7 +933,9 @@ function SessionScreenInner({ projectId, sessionId }: { projectId: string; sessi
if (assistantMsgs.length === 0) {
const startAt = sorted[lastUserIdx].time?.created ?? null
- return startAt == null ? null : { startAt, endAt: null as number | null, agent: optimisticAgent }
+ if (startAt == null) return null
+ const isOptimistic = lastUser.id.startsWith("local-")
+ return { startAt, endAt: null as number | null, agent: optimisticAgent, isSending: isOptimistic }
}
const startAt = assistantMsgs[0].time?.created ?? null
@@ -955,12 +957,22 @@ function SessionScreenInner({ projectId, sessionId }: { projectId: string; sessi
// The list is inverted, so the header renders visually below the newest message
const StreamingIndicator = useMemo(() => {
if (!turn || turn.startAt == null) return null
+ if (turn.isSending) {
+ return (
+
+
+
+ Sending message...
+
+
+ )
+ }
return (
)
- }, [turn])
+ }, [turn, theme])
const ListFooterComponent = useMemo(() => {
if (!isLoadingMore) return null
diff --git a/apps/mobile/components/hooks/event-stream.ts b/apps/mobile/components/hooks/event-stream.ts
index 2516cbf..9fd5560 100644
--- a/apps/mobile/components/hooks/event-stream.ts
+++ b/apps/mobile/components/hooks/event-stream.ts
@@ -350,6 +350,19 @@ export function useEventStream(url?: string, sessionId?: string, token?: string,
}
const msg = existing[msgIdx]
+ if (msg.role === "user") {
+ existing.push({
+ id: messageID,
+ sessionID: currentSessionId,
+ role: "assistant",
+ time: { created: Date.now() },
+ agent: "build",
+ model: { providerID: "...", modelID: "..." },
+ parts: [{ id: partID, type: "text", text: delta } as Part],
+ } as unknown as Message)
+ scheduleFlush()
+ break
+ }
const parts = msg.parts ?? []
const partIdx = parts.findIndex(p => p.id === partID)
if (partIdx >= 0) {
@@ -375,16 +388,15 @@ export function useEventStream(url?: string, sessionId?: string, token?: string,
let msgIdx = existing.findIndex(m => m.id === part.messageID)
if (msgIdx < 0) {
- // Part arrived before its message (common for user file parts).
- // Create a placeholder instead of dropping it.
let incoming = part as Part
if (incoming.type === "file" && localFileUriBackup.length > 0) {
incoming = { ...incoming, url: localFileUriBackup.shift()! } as Part
}
+ const partRole = incoming.type === "text" ? "assistant" : "user"
existing.push({
id: part.messageID,
sessionID: currentSessionId,
- role: "user",
+ role: partRole,
time: { created: Date.now() },
agent: "build",
model: { providerID: "...", modelID: "..." },
@@ -643,7 +655,11 @@ export function useEventStream(url?: string, sessionId?: string, token?: string,
reconnectAttempts++
const delay = reconnectDelays[Math.min(reconnectAttempts - 1, reconnectDelays.length - 1)]
+ // Only show reconnecting/connectivity-issues after repeated failures
+ // to avoid banner flicker on brief, self-healing stream drops
if (reconnectAttempts <= 2) {
+ // Keep "connected" — the stream may recover on its own
+ } else if (reconnectAttempts <= 4) {
setConnectionStatus("reconnecting")
} else {
setConnectionStatus("connectivity-issues")
diff --git a/apps/mobile/components/session-header.tsx b/apps/mobile/components/session-header.tsx
index 3b05b39..f39b1a9 100644
--- a/apps/mobile/components/session-header.tsx
+++ b/apps/mobile/components/session-header.tsx
@@ -80,7 +80,7 @@ function SessionHeaderInner({
className="flex flex-row gap-2 items-center border-b border-accent pb-2 px-4"
style={{ paddingTop: paddingTop + 10 }}
>
-