diff --git a/web/mobile/src/features/chat/Composer.tsx b/web/mobile/src/features/chat/Composer.tsx index 126aa8e953f..baa65e33d51 100644 --- a/web/mobile/src/features/chat/Composer.tsx +++ b/web/mobile/src/features/chat/Composer.tsx @@ -34,6 +34,7 @@ export const Composer = ({ streaming = false, onStop, inputRef, + placeholder, }: { sessionId: string onSend: (input: {text: string; parts?: FileUIPart[]}) => void | Promise @@ -46,6 +47,8 @@ export const Composer = ({ onStop?: () => void /** Lets the host write into the input — a rewind puts the rewound message back to edit. */ inputRef?: MutableRefObject + /** Full placeholder override — used when the composer is gated (no model key). */ + placeholder?: string }) => { const attachments = useComposerAttachments({sessionId}) const ownInputRef = useRef(null) @@ -137,6 +140,7 @@ export const Composer = ({ disabled={disabled} composerDisabled={disabled} dictating={dictating} + placeholder={placeholder} waitingOnUser={waitingOnUser} streaming={streaming} onStop={onStop} diff --git a/web/mobile/src/features/chat/LiveConversation.tsx b/web/mobile/src/features/chat/LiveConversation.tsx index 932404826f9..d6c32f1195a 100644 --- a/web/mobile/src/features/chat/LiveConversation.tsx +++ b/web/mobile/src/features/chat/LiveConversation.tsx @@ -14,14 +14,20 @@ import {useAgentConversation, useAgentModelKeyStatus, useConnectionDock} from "@ import {getPendingApprovals, type TurnViewModel} from "@agenta/chat/model" import {AgentIntroCard} from "@agenta/entity-ui/agent" import {modal} from "@agenta/ui/app-message" -import {ChatJumpToLatest} from "@agenta/ui/components/presentational" +import { + ChatBubble, + ChatBubbleAvatar, + ChatJumpToLatest, + turnRowClass, +} from "@agenta/ui/components/presentational" import type {RichChatInputHandle} from "@agenta/ui/rich-chat-input" -import {useSetAtom} from "jotai" +import {useAtomValue, useSetAtom} from "jotai" +import {User} from "lucide-react" import {ContentRail} from "@/components/ContentRail" import {ScreenScaffold} from "@/components/ScreenScaffold" -import {takePendingTaskAtom} from "../home/pendingTask" +import {pendingTasksAtom, takePendingTaskAtom} from "../home/pendingTask" import {AppShell} from "../nav/AppShell" import {ApprovalDock} from "./ApprovalDock" @@ -114,6 +120,12 @@ export const LiveConversation = ({ // vault says one already exists). The guard holds the SESSION it // fired for, not a bare flag: this component survives a session switch, and a flag would // swallow the next session's stashed task. + + // Peek at the parked task WITHOUT consuming it — used only for display while the gate holds. + // `takePendingTaskAtom` removes the entry; this read leaves it in place for the send effect. + const pendingTasks = useAtomValue(pendingTasksAtom) + const heldTaskText = pendingTasks[sessionId]?.text ?? null + const takePendingTask = useSetAtom(takePendingTaskAtom) const sentPendingTaskFor = useRef(null) const [pendingTaskError, setPendingTaskError] = useState(null) @@ -246,6 +258,30 @@ export const LiveConversation = ({ } else { body = ( + {/* A task typed before any provider key exists is held in `pendingTasksAtom` + (not yet sent — the gate is up). Render it as a user bubble so the person + can see what they wrote, matching desktop parity: the desktop shows the + held seed above the connect-model banner. Cleared the moment the gate + drops and the send effect fires (`takePendingTaskAtom` removes the entry). */} + {heldTaskText ? ( +
+ } />} + className="min-w-0 max-w-[85%]" + classNames={{ + content: "min-w-0 max-w-full overflow-hidden text-xs", + body: "min-w-0 max-w-full overflow-hidden", + }} + content={ + + {heldTaskText} + + } + /> +
+ ) : null} {conversation.isEmpty ? ( // The SAME card the desktop shows a conversation with no messages: who you are // about to talk to. A blank session is not an error state — /m rendered nothing @@ -364,6 +400,9 @@ export const LiveConversation = ({ sessionId={sessionId} onSend={({text, parts}) => conversation.send({text, parts})} disabled={conversation.isHydrating || modelBlocked} + placeholder={ + modelBlocked ? "Connect a model to start chatting…" : undefined + } waitingOnUser={conversation.hitlPending} streaming={streamingHere} onStop={conversation.stop}