Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions web/mobile/src/features/chat/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const Composer = ({
streaming = false,
onStop,
inputRef,
placeholder,
}: {
sessionId: string
onSend: (input: {text: string; parts?: FileUIPart[]}) => void | Promise<void>
Expand All @@ -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<RichChatInputHandle | null>
/** Full placeholder override — used when the composer is gated (no model key). */
placeholder?: string
}) => {
const attachments = useComposerAttachments({sessionId})
const ownInputRef = useRef<RichChatInputHandle | null>(null)
Expand Down Expand Up @@ -137,6 +140,7 @@ export const Composer = ({
disabled={disabled}
composerDisabled={disabled}
dictating={dictating}
placeholder={placeholder}
waitingOnUser={waitingOnUser}
streaming={streaming}
onStop={onStop}
Expand Down
45 changes: 42 additions & 3 deletions web/mobile/src/features/chat/LiveConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<string | null>(null)
const [pendingTaskError, setPendingTaskError] = useState<string | null>(null)
Expand Down Expand Up @@ -246,6 +258,30 @@ export const LiveConversation = ({
} else {
body = (
<ContentRail className="flex grow flex-col gap-3 p-4 pb-[calc(1rem+env(safe-area-inset-bottom))]">
{/* 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 ? (
<div className={`${turnRowClass} justify-end`}>
<ChatBubble
placement="end"
variant="filled"
avatar={<ChatBubbleAvatar icon={<User className="size-4" />} />}
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={
<span className="whitespace-pre-wrap break-words">
{heldTaskText}
</span>
}
/>
</div>
) : 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
Expand Down Expand Up @@ -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}
Expand Down
Loading