Skip to content

feat(mobile): first-run hero and the provider key gate, like desktop - #6167

Merged
mmabrouk merged 7 commits into
release/v0.113.0from
feat/mobile-provider-key-gate
Aug 21, 2026
Merged

mmabrouk merged 7 commits into
release/v0.113.0from
feat/mobile-provider-key-gate

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

What a brand-new user meets on /m: the first-run hero, the provider key gate, and (added during release QA) the fix for the first message to a new agent failing. Based on release/v0.113.0 (retargeted after #6152 merged).

The gap

Desktop never shows a new user its Home. OnboardingEntry.tsx:45 sees an empty agents list (useAgentsFirstRun, web/oss/src/components/pages/agents/store.ts:75) and redirects into the playground, which asks "What do you want to build?" with the composer ready. And it tells the user about a missing provider key before the send: useAgentModelKeyStatus(entityId).gateActive (web/packages/agenta-chat/src/hooks/useAgentModelKeyStatus.ts:82, rule at :67) disables the composer (AgentConversation.tsx:194, :265), raises ConnectModelBanner.tsx:36, and parks the first-run seed until a key lands (useFirstRunSeed.ts:169).

/m had neither.

  • A new user landed on Home, which asks what you want to do, shows an empty agents panel, and renders no composer at all — Home's composer runs a task with an agent that already exists, so with none it does not mount (HomeScreen.tsx:96). One "New agent" button on an empty page was the whole first-run experience.
  • The first message then went out on an empty vault, the runner answered 422 no usable credential (sdks/python/agenta/sdk/agents/connections/errors.py:40), and the user got a red "The agent run failed" bubble with a raw backend reason (TurnRow.tsx:303) and nothing to click. The vault lived only in Settings, which a new user has no reason to open.

useAgentConversation.ts:16 says the key gate is left to the skin on purpose. The mobile skin never composed it.

1. The provider key gate (590df9ed3c)

Nothing had to be extracted: the hook is already in @agenta/chat/hooks, the vault atoms in @agenta/entities/secret, and /m already ships a provider-key sheet in Settings.

  • features/chat/ConnectModelStrip.tsx — the mobile skin of the desktop banner. Enters and leaves through RevealCollapse, the shared composer-chrome idiom this app already uses for the mic notice. Its button opens this app's ProviderKeySheet scoped to the agent's own provider (providerEntry), rather than desktop's all-providers drawer plus a model repoint (useOnboardingProviderSetup). One field, no config write, same end state — and that repoint is the only part of the desktop path that is app-layer.
  • features/chat/pendingTaskPolicy.ts — the parked-task rule, pure and tested. A task typed on Home now waits while the gate is up and goes out on its own when a key is saved, matching useFirstRunSeed. It also waits while the VAULT is still unresolved — gateActive is deliberately false then, and on a cold first run hydration can settle first, which sent the message before anything knew the project was keyless. That wait is bounded at 10 s: the vault query has no retry cap, and unlike a hold on an active gate (strip up, composer disabled) an unresolved-vault hold is invisible. On the deadline the task is released, its text goes back into the composer, and a line says it was not sent.
  • features/chat/LiveConversation.tsx — composes the hook, disables the composer, renders the strip, routes the parked send through the policy.

gateActive is false while the vault query is unresolved, so a reload cannot flash a false "add a key" at a project that already has one. LiveConversation is the single choke point for every mobile entry into a chat, so one mount covers them all.

2. The first-run surface (5268f9173d)

Rendered in place, not behind a route of its own. The redirect is what forces desktop's detection to be certain before it decides; a swap under the same URL corrects itself when the list lands, and it keeps the nav drawer — which is how a keyless user still reaches Settings.

  • features/onboarding/homeSurface.ts — the three-way rule (loading / first-run / home), pure and tested. Keeps desktop's two real constraints: a failed fetch is never evidence of emptiness (it falls through to Home, the retryable surface), and an empty list that is still resolving holds a skeleton rather than flashing Home. A cached non-empty list short-circuits the hold, so a returning user never waits.
  • features/onboarding/FirstRunScreen.tsx — the desktop hero's own words (lifted from AgentChatEmptyState.tsx:14 and agent-home/assets/constants.ts:67, copied rather than imported because /m may not import the desktop app layer), the composer, and the shared create button for templates.
  • features/onboarding/FirstRunComposer.tsx — HomeTaskComposer cannot serve this: it runs a task with an agent that already exists and disables itself without one. So this binds the same underlying ChatComposer to the create path. Starter chips fill the input rather than send it — a phone keyboard makes "describe an agent" expensive, and an editable half-idea beats a blank box.
  • useNewAgentAction.createFromPrompt — the first submit runs the SAME shared mint+commit core (@agenta/home-ui useCreateAgent) that the blank and template creates run, then stashes what was typed against the session it minted and hands off to the chat. run now reports whether the hand-off committed, because a caller cannot read error after an await — that flag belongs to the render it was read in.
  • features/agents/agentHandoff.ts — the hand-off route, extracted pure and tested.

The chain, end to end

FirstRunScreen submit → createFromPrompt → shared useCreateAgent mints + commits (default name "New agent", the same default desktop onboarding uses) → invalidateWorkflowsListCache → stash {agentId, text, parts} against the minted session → router.push(/sessions/<id>?agent=<appId>) → ChatScreen → LiveConversation → useAgentModelKeyStatus → gate up on a keyless project → canSendPendingTask returns false, the task stays stashed, ConnectModelStrip shows → user saves a key → useVaultSecret sets providerKeySetupDone and the connections list grows → gateActive false → the effect re-runs and the parked message sends.

Trade-off against desktop (deliberate)

Desktop mints an ephemeral, client-only agent (useAgentOnboarding.ts:142) and commits it on send, so abandoning the hero leaves nothing behind. Here the first submit creates the real agent — which is exactly what this app's "New agent" button already does. Matching desktop would mean porting the ephemeral commit-in-place dance and its ~250 lines of playground state. Accepted for now; the visible cost is an abandoned "New agent" in the roster if the user submits and then leaves.

3. M3: the first message to a new agent failed (f2e1d963e8)

Found in release QA on oss.preview, and it predates this PR. From /m, the first message to a
freshly created agent came back as a failed turn:

The agent run failed. This agent workflow has no invocation URL — it can't be run yet.

The same agent ran fine from the desktop playground a minute later, and from /m on retry.

Root cause. buildAgentRequest returns null when the workflow entity has not loaded its
data.url / data.uri yet. That is a not yet, not a never: the create hand-off navigates,
resolves the agent's latest revision over the network (useAgentEntity), mounts the conversation,
and fires the stashed first message as soon as the transcript settles — often before the entity
that carries the invocation URL has landed. The shared engine
(useAgentConversation.prepareSendMessagesRequest) threw on the FIRST null, so that race became a
failed run.

The desktop never hit this because useAgentChatSession has bounded the same build since
#6042 — buildRequestWithinDeadline, whose own
comment names this exact case: "a navigation race, gone within moments". The shared hook /m runs
on never got that fix. The retry working on /m is the same evidence: by then the entity is in the
store.

Fix — one copy for both surfaces. boundedRequest moves from the oss chat slice into
@agenta/chat/assets, the shared engine wraps its build in it, and the desktop imports it from the
package instead of its own relative path. A not-yet-loaded workflow now retries every 300 ms for up
to 15 s and then fails with a clear message, instead of failing instantly. The deadline also caps a
build that HANGS, which is what it was written for.

Regression test in packages/agenta-chat/tests/unit/hooks/useAgentConversation.test.ts: a build
that returns null twice before yielding a request still produces an answered turn.

Follow-on, not fixed here. After the failed turn, a reload said "This session's earlier messages
are no longer stored" and dropped the turn. That is correct behavior for its inputs — the send never
reached the server, so the session has no durable records, and a reload loses the in-memory
"fresh session" marker. It stops happening once the first send succeeds. Telling a never-run session
apart from a pruned one needs a server signal, so it is left alone rather than papered over.

QA (brand-new account, on a phone or an emulated mobile UA)

First run, no provider key — the whole path:

  1. Sign up and land on /m. You should see "What do you want to build?" with a composer, not an empty Home. No flash of Home first.
  2. Tap a starter chip. It fills the input; it must not send.
  3. Type and send. An agent is created and the chat opens.
  4. The strip reads "Add your model provider key to run this agent.", the composer is disabled, and your message has not been sent — it is parked.
  5. Tap Add key. The sheet opens already scoped to the agent's provider. Save a valid key.
  6. The strip collapses, the composer enables, and the parked message sends itself. No second tap.

M3 — the first message to a new agent (the fix above):
11. On a project that already has a provider key, create a brand-new agent from /m and send a
message immediately, without waiting. It must answer. It must NOT come back with "This agent
workflow has no invocation URL". Repeat it a few times: this was a race, so one pass proves less
than five.
12. Do the same from the /m Home composer with the agent picker on "New agent" — the path release
QA originally reproduced it on.

Regressions to check:
7. "Or start from a template" on the first-run screen creates the agent and opens the chat, same as before.
8. Reload /m with the agent now existing: normal Home, with its composer. The first-run screen must not come back.
9. A project that already has a key: reload the chat a few times. The strip must never flash.
10. Kill the network and load /m: the agents fetch fails, and you get Home (retryable), never the first-run hero.

Verification

Check Result
pnpm --filter @agenta/mobile test 15 files, 114 tests passed
pnpm --filter @agenta/chat test 39 files, 371 tests passed (M3 regression test included)
tsc --noEmit (@agenta/mobile) exit 0
tsc --noEmit (@agenta/oss) exit 0 — M3 moved a file the desktop imports
pnpm --filter @agenta/chat check (types + lint) exit 0
pnpm --filter @agenta/mobile lint exit 0 (5 pre-existing warnings, none in touched files)
pnpm --filter @agenta/mobile build exit 0, 18 routes built
prettier 3.7.4 --check on touched files clean

Not run: live QA on a device — the steps above are for the release QA pass.

…esktop

A brand-new project has no provider key. On desktop the chat says so before the
send: `useAgentModelKeyStatus().gateActive` raises `ConnectModelBanner` above the
composer, disables the composer, and parks the first-run seed until a key lands.

/m had none of it. The send went out, the runner answered 422 "no usable
credential", and the user got a red "The agent run failed" bubble carrying a raw
backend reason, with nothing to click. The vault lived only in Settings, which a
new user has no reason to open.

Mount the same gate in the mobile chat skin. The engine leaves this to the skin
on purpose (`useAgentConversation` says so), and the hook, the vault atoms, and
the key sheet were all already shared or already in this app.

- `ConnectModelStrip` — the mobile skin of the desktop banner, entering and
  leaving through `RevealCollapse` like the mic notice already does. Its button
  opens THIS app's `ProviderKeySheet` scoped to the agent's own provider, rather
  than desktop's all-providers drawer plus a model repoint: one field, no config
  write, same end state.
- `pendingTaskPolicy` — the parked-task rule, extracted pure and tested. A task
  typed on Home now waits while the gate is up and goes out on its own the
  moment a key is saved, which is what desktop's `useFirstRunSeed` does.

The gate stays down while the vault query is unresolved (`gateActive` is false
when `loading`), so a reload cannot flash a false "add a key" at a project that
has one.
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request frontend labels Aug 21, 2026
@vercel

vercel Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 21, 2026 6:05pm

Request Review

@mmabrouk mmabrouk changed the title feat(mobile): show the provider key gate after the first message, like desktop feat(mobile): show the provider key gate before the first run, like desktop Aug 21, 2026
@CLAassistant

CLAassistant commented Aug 21, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a first-run mobile onboarding experience with starter prompts, templates, and an instant chat composer.
    • Added agent creation from prompts, including file attachments and automatic navigation to new agents or sessions.
    • Added a banner prompting users to connect a required provider key.
  • Improvements

    • Improved home-screen loading, empty, and error states.
    • Prevented messages from sending until conversations and credentials are ready.
    • Improved duplicate-send prevention, session handling, and recovery from creation or delivery failures.
    • Added retries when chat requests are not immediately ready.

Walkthrough

The mobile app now supports first-run agent creation, seeded session handoff, and model-key gating. Chat request preparation retries unavailable workflow requests within a deadline and uses shared asset exports.

Changes

Mobile agent onboarding and conversation flow

Layer / File(s) Summary
Home surface resolution
web/mobile/src/features/onboarding/homeSurface.ts, web/mobile/src/features/onboarding/copy.ts, web/mobile/src/features/onboarding/states/FirstRunStates.tsx, web/mobile/src/features/home/HomeScreen.tsx, web/mobile/tests/unit/homeSurface.test.ts
The home screen selects home, loading, or first-run content from agent query state. It adds onboarding copy, starter prompts, animated transitions, and loading placeholders.
First-run creation and handoff
web/mobile/src/features/agents/agentHandoff.ts, web/mobile/src/features/agents/useNewAgentAction.ts, web/mobile/src/features/onboarding/FirstRunComposer.tsx, web/mobile/src/features/onboarding/FirstRunScreen.tsx, web/mobile/tests/unit/agentHandoff.test.ts
Prompt creation accepts session IDs and file parts. Attachments remain staged until successful creation. Seeded sessions use the shared handoff route.
Pending conversation credential gate
web/mobile/src/features/chat/pendingTaskPolicy.ts, web/mobile/src/features/chat/ConnectModelStrip.tsx, web/mobile/src/features/chat/LiveConversation.tsx, web/mobile/tests/unit/pendingTaskPolicy.test.ts
Pending tasks wait for transcript hydration and model credentials. The conversation shows a provider-key action and disables the composer while blocked.

Bounded request preparation

Layer / File(s) Summary
Request preparation deadline
web/packages/agenta-chat/src/assets/boundedRequest.ts, web/packages/agenta-chat/src/assets/index.ts, web/packages/agenta-chat/src/hooks/useAgentConversation.ts, web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts, web/packages/agenta-chat/tests/unit/assets/boundedRequest.test.ts, web/packages/agenta-chat/tests/unit/hooks/useAgentConversation.test.ts
Request construction retries null results until a deadline, reports distinct timeout states, and is used by agent conversations through shared imports. Tests cover delayed workflow readiness and successful retry completion.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to ed7a7

The new mobile first-run and provider-key flow can still lose a submission while provider status is unresolved, and timed-out tasks can drop attachments or leave stale errors visible in later chats. These bounded correctness issues should be fixed or explicitly accepted before merging.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FirstRunComposer
  participant useNewAgentAction
  participant LiveConversation
  participant ProviderKeySheet
  participant buildRequestWithinDeadline
  participant buildAgentRequest

  User->>FirstRunComposer: Submit prompt with optional attachments
  FirstRunComposer->>useNewAgentAction: createFromPrompt(text, sessionId, parts)
  useNewAgentAction->>LiveConversation: Navigate to selected route
  LiveConversation->>LiveConversation: Check hydration and model-key gate
  LiveConversation->>ProviderKeySheet: Open provider-key setup when blocked
  ProviderKeySheet-->>LiveConversation: Model-key gate clears
  LiveConversation->>buildRequestWithinDeadline: Prepare send request
  buildRequestWithinDeadline->>buildAgentRequest: Build request
  buildAgentRequest-->>buildRequestWithinDeadline: Return request or null
  buildRequestWithinDeadline-->>LiveConversation: Return request after retry or bounded error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 20 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: mobile first-run onboarding and provider-key gating.
Description check ✅ Passed The description directly explains the onboarding, provider-key gate, retry fix, tests, and QA scope in the changeset.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mobile-provider-key-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk
mmabrouk changed the base branch from fix/mobile-parity-and-shared-auth to release/v0.113.0 August 21, 2026 16:33
@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6167.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6167-58c2b56
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-21T18:17:22.367Z

… Home

Desktop never shows a new user its Home. `OnboardingEntry` sees an empty agents
list and redirects into the playground, which asks "What do you want to build?"
with the composer ready.

/m sent them to Home, which is the wrong screen for someone with nothing: it
asks what you want to DO, shows an empty agents panel, and renders no composer
at all — Home's composer runs a task with an agent that already exists, so with
none it does not mount. One "New agent" button on an empty page was the entire
first-run experience.

Render a first-run surface in that frame instead, in place rather than behind a
redirect. The redirect is what forces desktop's detection to be certain before it
decides; a swap under the same URL corrects itself when the list lands, and keeps
the nav drawer, which is how a keyless user still reaches Settings.

- `onboarding/homeSurface` — the three-way rule, pure and tested. Keeps desktop's
  two real constraints: a failed fetch is never evidence of emptiness, and an
  empty list that is still resolving holds a skeleton rather than flashing Home.
  A cached non-empty list short-circuits the hold.
- `onboarding/FirstRunScreen` — the desktop hero's own words, the composer, and
  the shared create button for templates.
- `onboarding/FirstRunComposer` — `HomeTaskComposer` cannot serve this (it needs
  an agent to exist and disables itself without one), so this binds the same
  `ChatComposer` to the create path. Starters FILL the input rather than send it:
  a phone keyboard makes "describe an agent" expensive.
- `useNewAgentAction.createFromPrompt` — the first submit runs the SAME shared
  mint+commit core the template and blank creates run, then stashes what was typed
  against the session it mints and hands off to the chat. `run` now reports
  whether the hand-off committed, because a caller cannot read `error` after an
  await: that flag belongs to the render it read it in.
- `agents/agentHandoff` — the hand-off route, extracted pure and tested.

The chain closes on the gate from the previous commit: first submit creates the
agent, opens the chat, and on a keyless project the typed message parks while the
strip asks for a key, then sends itself when the key lands.

Trade-off against desktop: desktop mints an EPHEMERAL client-only agent and
commits it on send, so abandoning the hero leaves nothing behind. Here the first
submit creates the real agent, which is what "New agent" already does on this app.
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Aug 21, 2026
@mmabrouk mmabrouk changed the title feat(mobile): show the provider key gate before the first run, like desktop feat(mobile): first-run hero and the provider key gate, like desktop Aug 21, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web/mobile/src/features/onboarding/homeSurface.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep in-code comments concise and limited to surprising constraints. Remove product-history and design-rationale narration across the listed files; retain only short notes for bugs, races, or ordering requirements.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 07b9c3bc-cb15-4d3a-bbd9-9c4d13c09933

📥 Commits

Reviewing files that changed from the base of the PR and between ddd5f2f and 5268f91.

📒 Files selected for processing (14)
  • web/mobile/src/features/agents/agentHandoff.ts
  • web/mobile/src/features/agents/useNewAgentAction.ts
  • web/mobile/src/features/chat/ConnectModelStrip.tsx
  • web/mobile/src/features/chat/LiveConversation.tsx
  • web/mobile/src/features/chat/pendingTaskPolicy.ts
  • web/mobile/src/features/home/HomeScreen.tsx
  • web/mobile/src/features/onboarding/FirstRunComposer.tsx
  • web/mobile/src/features/onboarding/FirstRunScreen.tsx
  • web/mobile/src/features/onboarding/copy.ts
  • web/mobile/src/features/onboarding/homeSurface.ts
  • web/mobile/src/features/onboarding/states/FirstRunStates.tsx
  • web/mobile/tests/unit/agentHandoff.test.ts
  • web/mobile/tests/unit/homeSurface.test.ts
  • web/mobile/tests/unit/pendingTaskPolicy.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

sessionId,
sentFor: sentPendingTaskFor.current,
hydrating: isHydrating,
modelBlocked,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

[P1] Wait for vault resolution before consuming the pending task

gateActive is intentionally false while modelKey.loading is true. On a cold first-run session, hydration can finish first, so this effect consumes and sends the pending task before the vault tells us the project is keyless. That first run fails with 422, and there is no task left to send after the user adds a key. Hold the pending task while modelKey.loading is true, while continuing to hide the connect-model strip until loading resolves.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e23df7b. The race is real: gateActive is false while the vault loads, so a cold first run where hydration settles first sent the parked message straight into a 422, and the slot was already consumed.

canSendPendingTask now takes modelKeyLoading and holds on it, and LiveConversation passes modelKey.loading. The strip is untouched, so it still keys off gateActive and stays hidden until loading resolves. Two tests in pendingTaskPolicy.test.ts cover the cold-start order.

One caveat to flag, because it is a real trade: loading is !Array.isArray(vaultQuery.data), and the vault query sets no retry override, so a vault fetch that keeps failing leaves loading true forever. The parked task then waits instead of failing with a 422. The composer stays usable, but the parked text is no longer visible to the user. That is a silent loss where before it was a loud error. If you want it bounded, the desktop has the pattern already (useFirstRunSeed waits at most 10s for the build-kit overlay, then sends anyway). Say the word and I will add the same bound here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Bound added in ed7a7e7: the parked task now waits at most 10 seconds for the vault, then releases, puts its text back in the composer, and shows "Couldn't check this project's provider keys — the message was not sent. Please try again." A hold on an ACTIVE gate stays unbounded, because that one is visible and the user releases it by saving a key. 7 unit tests on the bound.


const seed = params?.seedMessage?.trim()
const sessionId = seed ? crypto.randomUUID() : null
const sessionId = seed ? (params?.sessionId ?? crypto.randomUUID()) : null

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

[P2] Treat attachment-only submits as seeded handoffs

ChatComposer intentionally calls onSubmit("") when attachments are ready but the text is empty. Here seed becomes falsy, so sessionId becomes null even when seedParts is non-empty. The parts are never stashed, navigation falls back to the agent overview, and the caller clears the attachments after a reported successful handoff. Treat non-empty seedParts as a seeded session and stash them even when the text is empty.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 059a1c1. Confirmed at the source: SendButton.tsx:46 calls onSubmit("") when the editor is empty and the staged files have settled, so an attachment-only first message left seed falsy, minted no session, stashed no parts, landed on the agent overview, and then had its attachments cleared as a success.

A create is now seeded by text OR parts. The decision moved into agentHandoff.ts as isSeededCreate({seed, partCount}), which keeps it beside the route it decides and makes it testable in the node suite. Three cases are covered in agentHandoff.test.ts. The stash carries text: "" with the parts, which the engine accepts: useAgentConversation.send only refuses when the text, the files and the parts are all empty.

const parts = staged.length > 0 ? stagedFilesToParts(staged, sessionId) : undefined
// The outcome comes back as a value, not off `newAgent.error`: that flag belongs to THIS
// render, so reading it after the await would read the state from before the create.
const handedOff = await newAgent.createFromPrompt({text, sessionId, parts})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

[P2] Restore the first-run draft when creation fails

RichChatInput clears the editor after submit before this promise resolves. When agent creation or navigation returns false, this branch preserves the attachments but never restores text, so a transient failure loses the entire agent description. Restore the submitted markdown through inputRef when handedOff is false.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 69ef26b. When handedOff is false, the submitted markdown goes back into the editor through inputRef.current.setMarkdown(text).

One guard on top of that: it refills only an editor that is still empty (getMarkdown().trim()). The create is awaited, so the user can start typing a new idea while it runs, and an unconditional refill would overwrite what they just typed.

* of them would shift into the other.
*/
export const FirstRunLoading = () => (
<div className="flex flex-col gap-6 px-4 pb-6 lg:px-16 lg:pt-14">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

[P3] Match the loading skeleton to the first-run frame

On wide viewports this container spans the scaffold and uses gap-6, while FirstRunScreen uses pageContentWidthClass and gap-5. The crossfade therefore shifts horizontally and vertically. Give the loading state the same width cap, gutters, and primary spacing as the first-run frame so the state swap preserves identical geometry.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 69ef26b. The loading state now carries the hero's exact frame: ${pageContentWidthClass} flex flex-col gap-5 px-4 pb-6 lg:px-16 lg:pt-14.

I also matched the inner structure, not only the outer box. The two title lines moved into a gap-2 block, as in FirstRunScreen, so the subtitle skeleton sits where the subtitle lands instead of one gap lower. The width cap is shared with Home as well, so the loading-to-Home swap keeps the same gutters too.

The connect-model gate reports `gateActive: false` while the vault query is still
in flight, on purpose — the banner must not flash on every reload. The pending-task
effect read that same flag, so on a cold first run, where hydration can settle before
the vault does, the parked message went out before anything knew the project was
keyless: a 422, and no task left to send once the user added a key.

The gate now takes `modelKeyLoading` as its own hold. The strip still stays hidden
until loading resolves.
…d hand-off

Two ways the create path reported a hand-off it had not made:

- The composer submits `""` once staged files settle, so a picture-only first
  message left `seed` falsy: no session was minted, the parts were never stashed,
  we landed on the agent overview, and the caller cleared the attachments. A create
  is now seeded by text OR parts (`isSeededCreate`).
- `router.push` RESOLVES false on a cancelled navigation instead of throwing, so
  only the throw was handled. Both outcomes now release the latch, drop the stash
  and return false.
…n a failed create

- The rich input clears itself on submit, so a failed create lost the whole agent
  description. It is restored through the input handle, unless the user has already
  started retyping.
- The starter pills had hover and disabled states but no keyboard focus state; they
  now carry the app's focus ring (`focus-visible:border-ring` + `ring-[3px]`).
- The loading skeleton spanned the scaffold at `gap-6` while the hero uses the page
  width cap at `gap-5`, so the crossfade shifted. Same frame now.
@mmabrouk

Copy link
Copy Markdown
Member Author

Review pass on 6167 — everything answered

All six inline findings are fixed and pushed. Head is now 69ef26b588. Each inline thread has a reply with the detail; this comment covers the one item that lives only in the review summary.

Finding Where Outcome
[P1] Wait for vault resolution before consuming the pending task LiveConversation.tsx Applied — e23df7b541
[P2] Treat attachment-only submits as seeded handoffs useNewAgentAction.ts Applied — 059a1c1c59
[P2] Restore the first-run draft when creation fails FirstRunComposer.tsx Applied — 69ef26b588
[P3] Match the loading skeleton to the first-run frame FirstRunStates.tsx Applied — 69ef26b588
CodeRabbit: handle a cancelled navigation result useNewAgentAction.ts Applied — 059a1c1c59
CodeRabbit: add a visible keyboard focus state FirstRunComposer.tsx Applied — 69ef26b588, with this app's ring idiom
CodeRabbit nitpick: keep in-code comments concise homeSurface.ts and the other new files Not applied — see below

The comment nitpick: not applied, and why

The rule it cites is real. web/AGENTS.md:437 says at most one short line per comment, and several new files here open with a multi-line block that narrates why. ConnectModelStrip.tsx is the clearest case: a paragraph of product history about the 422 bubble that /m used to show.

I left them, for two reasons. The blocks carry the desktop-parity rationale that the code cannot show — which rule of useAgentsFirstRun was kept and which was dropped, why the strip asks for one provider key where desktop opens a full drawer, why the starters fill the input instead of sending it. A reviewer needs that, and there is no design doc in this PR holding it instead. Second, you wrote them and reviewed these same files without flagging them, so trimming them is your call, not a bot's.

If you do want the trim, it is one commit and I will do it on request. My suggestion would be to cut the product-history passages and keep the parity rules.

Verification on the pushed head

pnpm --filter @agenta/mobile test (15 files, 107 tests, all pass — 5 new), tsc --noEmit clean, pnpm --filter @agenta/mobile lint clean (5 pre-existing warnings, no errors), pnpm --filter @agenta/mobile build succeeded on Next 16 with all 18 routes, and prettier@3.7.4 --check passes on every touched file.

Behavior is unchanged where it should be: the composer still locks behind the key gate, the strip still stays hidden until the vault answers, a failed agent list still shows Home rather than the hero, and the first submit still creates the real agent and opens its chat.

One trade is worth your attention before merge, and it is written up in full on the P1 thread: holding the parked task on modelKey.loading means a vault query that keeps failing holds the task forever instead of failing loudly with a 422. The desktop bounds a similar wait at 10 seconds. Tell me if you want that bound here.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/mobile/src/features/chat/LiveConversation.tsx (1)

270-281: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Disable the Composer while the model-key status is unresolved.

modelBlocked is false while modelKeyLoading is true. After hydration completes, the Composer can therefore submit before the vault resolves. A keyless project can reject that message before the model gate appears, and this direct submission is not parked for retry.

Include modelKeyLoading in the Composer disable condition.

Proposed fix
-                        disabled={conversation.isHydrating || modelBlocked}
+                        disabled={conversation.isHydrating || modelKeyLoading || modelBlocked}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cb48bbf-4e6f-4a7a-afe5-2833672f8f84

📥 Commits

Reviewing files that changed from the base of the PR and between 5268f91 and 69ef26b.

📒 Files selected for processing (8)
  • web/mobile/src/features/agents/agentHandoff.ts
  • web/mobile/src/features/agents/useNewAgentAction.ts
  • web/mobile/src/features/chat/LiveConversation.tsx
  • web/mobile/src/features/chat/pendingTaskPolicy.ts
  • web/mobile/src/features/onboarding/FirstRunComposer.tsx
  • web/mobile/src/features/onboarding/states/FirstRunStates.tsx
  • web/mobile/tests/unit/agentHandoff.test.ts
  • web/mobile/tests/unit/pendingTaskPolicy.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

… (M3)

The first message to a newly created agent failed on /m with "This agent workflow
has no invocation URL — it can't be run yet." The same agent ran from the desktop
a minute later, and from /m on retry.

Root cause: the shared chat engine's `prepareSendMessagesRequest` threw on the FIRST
null from `buildAgentRequest`. Null there does not mean "not runnable" — it means the
workflow entity carrying `data.url`/`data.uri` has not been fetched yet. The first-run
hand-off navigates, resolves the agent's latest revision over the network, and fires
the stashed message as soon as the transcript settles, which is often before that
entity lands. The desktop never hit it because `useAgentChatSession` has bounded the
same build since #6042; the shared hook the mobile app uses never got that fix.

So give both surfaces one copy: `boundedRequest` moves from the oss chat slice into
`@agenta/chat/assets`, the shared engine wraps its build in it, and the desktop
imports it from the package. A not-yet-loaded workflow now retries for up to 15s and
fails with a clear message if the URL never appears, instead of failing instantly.

Also caps a build that HANGS, which is what the deadline was written for.
@mmabrouk

Copy link
Copy Markdown
Member Author

M3 fixed on this branch — f2e1d963e8

Release QA found that the first message to a NEW agent fails on /m with "This agent workflow has no invocation URL — it can't be run yet." It predates this PR, but it lives in the path this PR rewrites, so the fix lands here. The PR body now has a full section on it plus QA steps 11 and 12.

Root cause. buildAgentRequest returns null while the workflow entity carrying data.url / data.uri is still being fetched. That is a "not yet", not a "never". The first-run hand-off navigates, resolves the agent's latest revision over the network, and fires the stashed message as soon as the transcript settles — usually before that entity lands. The shared engine threw on the first null, so the race became a failed run.

The desktop was already immune, which is the proof. useAgentChatSession has bounded the same build since #6042, and that fix's own comment names this case: "a navigation race, gone within moments". The shared hook /m runs on never got it. The other half of the evidence is the QA observation itself: a retry works, so nothing changed but time.

The fix. boundedRequest moves from the oss chat slice into @agenta/chat/assets; the shared engine wraps its build in it; the desktop imports it from the package. A not-yet-loaded workflow retries every 300 ms for up to 15 s and then fails with a clear message. One copy, both surfaces, no new divergence. Regression test added to useAgentConversation.test.ts — a build that returns null twice still produces an answered turn.

Deliberately not done: passing the created revisionId through the hand-off (a good follow-up that would skip two round-trips, but it changes the route contract), and the "earlier messages are no longer stored" notice after a failed turn (correct for its inputs, and it stops happening once the first send succeeds).

Verification on f2e1d963e8: @agenta/chat 371 tests pass and its types plus lint are clean; web/oss tsc --noEmit clean (the desktop import moved); mobile 107 tests, tsc, lint, and the Next 16 build all clean; prettier clean on all six touched files. Not verified live — M3 is a timing bug, so step 11 asks for several consecutive first-sends rather than one.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: d6e0b8fe-aefc-4af5-b2e5-20d4f69c304c

📥 Commits

Reviewing files that changed from the base of the PR and between 69ef26b and f2e1d96.

📒 Files selected for processing (6)
  • web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts
  • web/packages/agenta-chat/src/assets/boundedRequest.ts
  • web/packages/agenta-chat/src/assets/index.ts
  • web/packages/agenta-chat/src/hooks/useAgentConversation.ts
  • web/packages/agenta-chat/tests/unit/assets/boundedRequest.test.ts
  • web/packages/agenta-chat/tests/unit/hooks/useAgentConversation.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/packages/agenta-chat/src/assets/boundedRequest.ts (1)

40-41: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Always yield after a null build.

If retryMs is 0 or negative, Line 41 skips the await. An immediately resolving null build then loops in the microtask queue until the deadline. This blocks rendering for up to 15 seconds.

Clamp the delay and await setTimeout even when the delay is zero.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: d6e0b8fe-aefc-4af5-b2e5-20d4f69c304c

📥 Commits

Reviewing files that changed from the base of the PR and between 69ef26b and f2e1d96.

📒 Files selected for processing (6)
  • web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts
  • web/packages/agenta-chat/src/assets/boundedRequest.ts
  • web/packages/agenta-chat/src/assets/index.ts
  • web/packages/agenta-chat/src/hooks/useAgentConversation.ts
  • web/packages/agenta-chat/tests/unit/assets/boundedRequest.test.ts
  • web/packages/agenta-chat/tests/unit/hooks/useAgentConversation.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.

Holding the first message until the vault answers fixed the cold-start 422, but the
vault query has no retry cap: an outage left `loading` true forever and parked the
message with nothing on screen to explain it. Unlike a hold on an active gate — which
the strip explains and the user releases by saving a key — that wait is invisible.

So bound it, the way the desktop bounds its own waits. After 10 seconds with the vault
still unresolved, the task is released, the text goes back into the composer, and a
line above it says the message was not sent. A hold on an ACTIVE gate stays unbounded,
because that one is the feature working.

The rule is now a three-way decision (hold / send / abandon) rather than a boolean,
since "stop holding" and "send" are different outcomes. 14 unit tests, 7 on the bound.
@mmabrouk
mmabrouk merged commit 87792cf into release/v0.113.0 Aug 21, 2026
38 of 40 checks passed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/mobile/src/features/chat/LiveConversation.tsx (1)

326-329: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not send a composer task while modelKeyLoading is true.

After hydration completes before vault resolution, the composer is enabled because modelBlocked is still false. Line 328 then calls conversation.send directly and bypasses pendingTaskDecision. A normal user submission can enter the keyless request path during the same unresolved-vault window. Queue these submissions through the gate, or disable submission until the vault resolves.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5da9447a-5720-452f-b977-0e5cc9e98719

📥 Commits

Reviewing files that changed from the base of the PR and between f2e1d96 and ed7a7e7.

📒 Files selected for processing (3)
  • web/mobile/src/features/chat/LiveConversation.tsx
  • web/mobile/src/features/chat/pendingTaskPolicy.ts
  • web/mobile/tests/unit/pendingTaskPolicy.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.

// swallow the next session's stashed task.
const takePendingTask = useSetAtom(takePendingTaskAtom)
const sentPendingTaskFor = useRef<string | null>(null)
const [pendingTaskError, setPendingTaskError] = useState<string | null>(null)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope pendingTaskError to the affected task.

pendingTaskError has no reset path. This component survives session changes, so a timeout for one session remains visible in a later session. The error also remains after the restored message sends successfully. Reset it when sessionId changes and clear it when the composer accepts a retry.

Also applies to: 321-329

Comment on lines 126 to +135
const task = takePendingTask(sessionId)
if (!task) return
// Consumed either way — a released task must not replay on the next render.
sentPendingTaskFor.current = sessionId
if (decision === "abandon") {
setPendingTaskError(PENDING_TASK_NOT_SENT_MESSAGE)
// Hand the text back so "try again" is one tap. The composer is usable here: the gate
// is not up, because an unresolved vault never raises it.
if (task.text) composerRef.current?.setMarkdown(task.text)
return

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Restore task.parts when the task is abandoned.

Line 126 consumes the task. Lines 130-135 restore only task.text. task.parts is only passed to send at line 137. An attachment-only task, or attachments attached to a text task, are lost after the vault deadline. Restore the complete task through composer draft state, or re-queue it until the user retries.

This branch was successfully deployed

1 active deployment
Preview — ed7a7e73 Deployed Aug 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request frontend size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants