Skip to content
Draft
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
79 changes: 78 additions & 1 deletion docs/design/agenta-mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ headless chat core shared between desktop and mobile skins).
| [plans/2026-07-12-wp0-sessions-query-and-stamping.md](./plans/2026-07-12-wp0-sessions-query-and-stamping.md) | WP0 residual plan — **EXECUTED** (see banner: re-audit corrections + execution deltas) |
| [plans/2026-07-12-wp3a-chat-headless-core.md](./plans/2026-07-12-wp3a-chat-headless-core.md) | WP3a plan — **EXECUTED under copy-extraction** (see banners: strategy change + task mapping) |
| [plans/2026-07-25-wp1-infra-tail.md](./plans/2026-07-25-wp1-infra-tail.md) | WP1 infra tail (prod image CI, compose, run.sh) — **EXECUTED** (Tasks 1-5, 7); fixed the latent entrypoint crash in the unbuilt mobile image. Only the first `workflow_dispatch` publish (Task 7 runbook) is still pending, and it's post-merge by design |
| [plans/2026-07-26-wp5-device-gate.md](./plans/2026-07-26-wp5-device-gate.md) | WP5 device gate (flag-gated middleware, both directions) — **READY TO EXECUTE**; default-off, T8 banner-retirement deferred to flag-flip |
| [plans/2026-07-26-wp5-device-gate.md](./plans/2026-07-26-wp5-device-gate.md) | WP5 device gate (flag-gated middleware, both directions) — **EXECUTED** (Tasks T1-T7); default-off, T8 banner-retirement deferred to flag-flip |

Wave-2 plans (WP2 auth + project drawer, WP3b mobile chat skin, WP4 product pages, WP5 device
gate) are **deliberately unwritten** — they must be planned against the real wave-1 code and the
Expand Down Expand Up @@ -65,6 +65,10 @@ What works right now: `cd web && pnpm dev-mobile` → http://localhost:3000/m re
shell (light+dark from the bridged palette); `pnpm build-mobile` produces a standalone server;
`pnpm --filter @agenta/mobile lint` enforces the bans + token sync; the dev compose stacks have
a routable `web-mobile` service (needs a dev-image rebuild to pick up the Dockerfile changes).
Opt-in in dev too: `run.sh --dev --with-mobile` (originally it rode `with-web` and auto-started,
but a live dev run showed the second Next dev server pushes an 8GB Docker VM into OOM-killing
the main web app's first big Turbopack compile — dmesg-confirmed `next-server` kills at ~4.5GB
RSS. Running both dev servers comfortably wants a 12GB+ VM).
Comment on lines +68 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the documented Compose entrypoint path.

The command does not state a working directory. Use hosting/docker-compose/run.sh and specify the required OSS or EE edition with the dev and mobile options.

As per coding guidelines, run the local dev stack through hosting/docker-compose/run.sh and use the same edition and image mode as the loaded environment.

Source: Coding guidelines


### WP0 residual — COMPLETE (2026-07-25, 9 commits, all dual-reviewed)

Expand Down Expand Up @@ -147,6 +151,71 @@ green. Task 5 re-verified (didn't re-add) that mobile lint and `@agenta/chat` un
already reached by the existing generic CI mechanisms (workflows 11 and 12); only the mobile
image build and `@agenta/mobile` typecheck needed a new job.

### WP5 device gate — EXECUTED (2026-07-26, 5 commits)

Ships the mobile device gate (design.md "Gate and routing") behind a runtime flag,
`AGENTA_MOBILE_GATE`, **default off**: with the flag off, request behavior is byte-identical to
today. `NoMobilePageWrapper` retirement (T8) is specified but deliberately **not executed** —
it ships only in the deployment window where the flag is actually flipped on. See
[plans/2026-07-26-wp5-device-gate.md](./plans/2026-07-26-wp5-device-gate.md) for the full task
breakdown and grounding facts.

| Commit | Content |
|--------|---------|
| `cf272e1227` | T1: `@agenta/shared/utils/mobileGate` — pure, framework-free decision core (detection, deep-link maps, cookie semantics, documented exceptions); 27 unit tests in the package's vitest harness |
| `6b3aaf5654` | T2: `web/oss/src/middleware.ts` + `web/ee/src/middleware.ts` — twin desktop forward-gate adapters (byte-identical, both NEW files) wrapping the shared core |
| `2df5e4d2a1` | T3: `web/mobile/src/middleware.ts` — reverse-gate middleware carrying a declared verbatim copy of the reverse-gate subset (mobile has zero workspace deps until WP2), new minimal mobile vitest harness, and the "View desktop site" `?view=desktop` escape link on the placeholder page |
| `7be0b8d528` | T4: plumb `AGENTA_MOBILE_GATE` through dev + gh compose files (default `false`), documented in both dev env examples |
| `9ac651525f` | T6: self-skipping Playwright UA-emulation smoke (`web/oss/tests/playwright/acceptance/mobile-gate/gate.spec.ts`), 6 tests, skips unless the runner asserts `AGENTA_MOBILE_GATE=true` |
Comment on lines +154 to +169

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the WP5 commit record.

The heading says five commits and the table lists five commits. The section also claims T1-T7 executed and later refers to a six-commit review. Add the T7 documentation commit and update the count, or correct the execution claim.

Also applies to: 182-182


**Verification highlights:**

- Build proof: all three apps (`oss`, `ee`, `mobile`) print a `ƒ Middleware` row after adding
their respective `src/middleware.ts`, confirming Next 15.5.18 picks up the placement.
- Zero new tsc signatures in `@agenta/oss` or `@agenta/ee` after wiring the middleware (T2).
Scope note from the combined post-execution review: the T6 Playwright spec itself adds 9
signatures to the `@agenta/oss` tsc run (TS2307 `@playwright/test` + implicit-any bindings) —
the same error class every existing acceptance spec under `tests/playwright/` already
produces, because playwright specs are type-checked by the `web/tests` harness, not oss tsc.
Accepted as precedented noise; the clean fix (excluding `tests/playwright` in
`web/oss/tsconfig.json`) is a separate cleanup, not part of this WP.
- Combined six-commit review verdict: **approve**. Every code block landed byte-identical to
the plan, the desktop matcher regex was independently confirmed correct against the compiled
middleware manifest (`/m` and `/m/*` excluded; `/models`-style paths still gated), and the
middleware bundle carries only the gate core (no transitive leaks). Two deferred hardening
notes for the agenta_cloud PR: make the gate cookies `secure` conditional on the forwarded
proto and add `httpOnly` (safe — nothing client-side reads them), and note that EE's first
deploy with these commits is the first live `ƒ Middleware` proof for EE (its local build
predates the middleware; the file is a byte-twin of the verified OSS one).
- **T5 live proof** (the load-bearing verification): same standalone binary, two runs, no
rebuild between them. Flag on (`AGENTA_MOBILE_GATE=true`): mobile UA on `/w` → `307` to `/m/`,
desktop UA on `/w` → `200`. Same binary, flag unset: mobile UA on `/w` → `200`, no redirect —
proving `process.env` in middleware is read at request time on the pinned standalone server,
not build-inlined. Also surfaced a **basePath-stripping observation**: at runtime Next strips
the `/m` basePath from `nextUrl.pathname` before the mobile middleware handler runs, but the
handler normalizes defensively either way (needed for unit tests, which construct
`NextRequest` directly and still see the `/m` prefix).
- **Live QA found two gate defects the review missed (both fixed):** (1) Turbopack's DEV
middleware sandbox exposes only `.env`-file vars, not the container's process env, so
`AGENTA_MOBILE_GATE` read `undefined` in `next dev` even with the container env set — dev
compose commands now mirror the flag into `.env.development.local` at container start
(prod standalone is unaffected; T5's runtime-read proof stands). (2) With `basePath`, the
bare root `/m` never matched the `"/((?!...).*)"` matcher (the root strips to an empty
string), leaving the landing page ungated in BOTH dev and prod — the matcher now carries an
explicit `"/"` entry. Unit tests construct `NextRequest` directly and bypass Next's matcher
layer entirely, which is why 27+8 green tests missed it; only a live end-to-end probe
caught both. Desktop twins are unaffected (no basePath; `/` 308s into gated `/w`).
- **T6 spec self-skip.** `--list` (which does not invoke `global-setup`) confirms the spec
discovers exactly the expected 6 tests. A real (non-`--list`) run against this worktree with
no stack running was attempted to observe the runtime skip directly, but `global-setup`
unconditionally authenticates against `AGENTA_WEB_URL` before any per-test `test.skip` logic
runs, so it fails with `ERR_CONNECTION_REFUSED` rather than reporting `6 skipped` — exactly the
fallback the plan anticipated. With the flag confirmed unset in the runner env (the CI
default), the `test.skip(!gateEnabled, ...)` predicate is proven to evaluate `true`, so the
6-skipped outcome is correct by construction; observing it as a live Playwright report is an
operator step against a running stack (flag-on run is likewise an operator step — see Open
items below).

## Resume runbook (from here)

1. **Plan wave-2** against the real code (WP2 auth/drawer → WP3b skin → WP4 pages → WP5 gate).
Expand Down Expand Up @@ -201,6 +270,14 @@ pnpm dev-mobile # → http://localhost:3000/m, check light+dark
- **Design-doc staleness:** `docs/designs/sessions/**` predates the streams-merge/turns model;
don't trust it over the code. The memory file `project_agenta_mobile_discovery` (assistant
memory) mirrors this handoff.
- **WP5 device gate flag-flip runbook (not yet run):** once WP2 (mobile auth) and WP4 (product
pages) are live, per deployment: set `AGENTA_MOBILE_GATE=true` in that deployment's env file,
recreate the `web`/`web-mobile` services, run the T6 Playwright smoke
(`web/oss/tests/playwright/acceptance/mobile-gate/gate.spec.ts`) against it to confirm 6
passed, then land T8 (the prepared, not-yet-executed `NoMobilePageWrapper` retirement commit)
from [plans/2026-07-26-wp5-device-gate.md](./plans/2026-07-26-wp5-device-gate.md). T8 remains
**specified but unexecuted** in that plan by design — it is coupled to this flip, not to WP5
landing.

## Follow-up tracks (post-wave-1, explicitly out of scope for now)

Expand Down
133 changes: 133 additions & 0 deletions docs/design/agenta-mobile/chat-headless-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# `@agenta/chat` headless core — dissection & contract

Companion to [design.md](./design.md) (decision g). Grounded in a line-level dissection of the
current `AgentChatSlice` (2026-07-12). This is the working reference for WP3a.

Legend: **E** = Engine (packaged/pure, reuse as-is) · **B** = Behavior (belongs in the headless
layer; today inline in components) · **P** = Presentation (skin owns).

## 1. Classification of current responsibilities

| Responsibility | Class | Where it lives today |
|---|---|---|
| Stream transport (stream↔batch negotiation, batch replay) | E | `assets/AgentChatTransport.ts` + `createNegotiatingFetch`, `agentChannelModeAtom` (`@agenta/playground`) |
| Request building (config/auth/references → body) | E | `buildAgentRequest` (`@agenta/playground`) |
| Turn capture, resume-after-approval, queue release gates | E | `@agenta/playground` `execution/*` (pure) |
| Render-hint map (`data-render` → `render.kind`) | E | `buildRenderMap`, `renderKindFor` (`execution/renderMap.ts`) |
| Transcript replay (records → `UIMessage[]`) | E | `assets/transcriptToMessages.ts` + `loadSession.ts` |
| Tool display resolution / value formatting | E | `assets/toolDisplay.ts`, `assets/toolFormat.ts` (pure registries) |
| Tool output summarization | B | inline in `ToolActivity.tsx:49-89` — pure logic trapped in a presentational file |
| Attachment validation / encoding | E | `assets/attachments.ts`, `assets/files.ts` (File-based, pure) |
| Attachment **state** shape | B/leak | antd `UploadFile` in `AgentConversation.tsx:358-370` and `state/sessionEphemera.ts:1,29` |
| Trace/usage extraction from metadata | E | `assets/trace.ts` |
| Session model + ephemera + expand state | E (leaks) | `state/sessions.ts`, `state/sessionEphemera.ts` (holds `UploadFile` + virtuoso `StateSnapshot`), `state/expandState.ts` |
| Turn grouping (active turn, lastUserIndex) | B | `AgentConversation.tsx:1677-1685` |
| Empty-turn collapsing predicates | B | `AgentConversation.tsx:167-179,1728` |
| Turn render model (tool folding, superseded-gate dedup, client-tool split) | B | `AgentMessage.tsx:329-416` |
| hasAnswer/noResponse/error derivation | B | `AgentMessage.tsx:270-312` |
| Client-tool dispatch registry | E→registry | `components/clientTools/{registry,meta}.tsx` (widgets are P) |
| Approval extraction (`getPendingApprovals`) | B | `ApprovalDock.tsx:33-45` |
| Approval body registry (by tool name) | E→registry | `components/approvals/registry.tsx` (bodies are P) |
| Queue orchestration | E (hook) | `hooks/useAgentChatQueue.ts` — the template for headless hooks |
| Model-key gate | E (hook) | `hooks/useAgentModelKeyStatus.ts` |
| Hydration sequencing (seed → skeleton vs hero → server hydrate → SWR revalidate) | B | `AgentConversation.tsx:571-603,891-913` |
| Session-status derivation + publish (error>awaiting>running>idle) | B | `AgentConversation.tsx:561,991-1004` |
| Error stamping onto turn | B | `AgentConversation.tsx:1025-1052` + `parseAgentRunError:191` |
| Persist-on-settle + expand-prune | B | `AgentConversation.tsx:1055-1082` |
| Self-commit / committed-revision handling | B | `AgentConversation.tsx:1090-1118` |
| Rewind orchestration | B (+E core, P confirm) | `AgentConversation.tsx:1637-1672`; pure scan in `assets/rewind.ts`; `modal.confirm` is P |
| Client-tool output settle → `addToolOutput` | B | `AgentConversation.tsx:616-635` |
| Elicitation parsing/validation/envelopes | E | `@agenta/shared/utils` (already extracted; only field rendering is antd) |
| Scroll engineering (SC-1..4, anchor, jump pill, virtuoso) | B, desktop-only | `AgentConversation.tsx:465-488,1140-1497` (~350 lines) — mobile uses native scroll |
| Bubble/avatar/toolbar, tool rows, approval chrome, queued chips, tray, empty/skeletons, markdown | P | `components/*`, `assets/markdown.tsx` (antd/x + Prism) |
| Right panel, turn inspector, onboarding hero, template strip | P, desktop/onboarding-only | already null-gated |

`AgentConversation.tsx` is roughly **65% behavior / 35% presentation**; the behavior is almost
entirely app-agnostic. Onboarding, build mode, inspectors, and virtualization are all cleanly
gated (nullable context / atoms / env flags) — the mobile skin simply omits them.

## 2. antd/desktop type leaks to neutralize

1. **`UploadFile` as canonical attachment state** — `AgentConversation.tsx:358-370,499-516`,
`state/sessionEphemera.ts:1,29`, `ComposerAttachments.tsx:13,26`. Core moves to `File[]` (or
neutral `PendingAttachment{file, uid, name}`); `filesToParts`/`validateIncoming` are already
File-based.
2. **`Bubble<ReactNode>` prop shaping** in `AgentMessage.tsx:630-648` + the loading-bubble
placeholders (`AgentConversation.tsx:1925-1930`) — stays in the desktop skin.
3. **antd-x `Actions` items as toolbar data** — `AgentMessage.tsx:561-618`. Contract uses neutral
`{key, label, icon, onClick}[]` action descriptors.
4. **react-virtuoso types in shared state** — `StateSnapshot` in `sessionEphemera.ts:2,21`,
`state/virtualization.ts`. Desktop-local; moves out of the shared store.

Minor: `Modal.useModal` for the rewind confirm (core returns a `RewindPlan`, skin renders the
confirm), `App.useApp` toasts, antd `Form` inside `ElicitationWidget` (contract logic already in
`@agenta/shared/utils` — the cleanest existing example of the desired split).

## 3. Headless hooks (Layer 2 API sketch)

```ts
useAgentConversation({entityId, sessionId}): {
messages: UIMessage[]
status: "ready" | "submitted" | "streaming" | "error"
runStatus: "idle" | "running" | "awaiting" | "error"
error?: ParsedRunError
turns: TurnViewModel[] // pre-grouped: active turn, empty-collapse
send(input: {text: string; files?: File[]}): void // routes through the queue
stop(): void
regenerate(id: string): void
rewind(message: UIMessage): RewindPlan // {sideEffects[], confirm()} — skin renders confirm
isHydrating: boolean
isEmpty: boolean
}

useTurnRenderModel(message, ctx): RenderItem[] // lifted from AgentMessage.tsx:329-416
useComposerAttachments({sessionId, limits}): {files, rejections, add, remove, clear, atMax, toParts}
useSessionHydration({sessionId})
useApprovalDock({messages, onRespond}): {current, count, respond, approveAll, renderer}
useClientToolDispatch()
useAgentChatQueue(...) // exists — moves in
useAgentModelKeyStatus(...) // exists — moves in
useConversationScroll(ref, {messages, status}) // DESKTOP-ONLY opt-in
Comment on lines +83 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(chat-headless-contract\.md|.*WP3.*|.*approval.*|.*Approvall.*|.*Approval.*)' || true

echo
echo "== target lines =="
if [ -f docs/design/agenta-mobile/chat-headless-contract.md ]; then
  sed -n '1,130p' docs/design/agenta-mobile/chat-headless-contract.md | cat -n
fi

echo
echo "== search useApprovalDock =="
rg -n "useApprovalDock|approval|Approval|approveAll|renderer" -S . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -n 200

echo
echo "== search WP3 contract snippets =="
rg -n "function useApprovalDock|useApprovalDock\\(|approveAll|registry|renderer|render" docs/design/agenta-mobile -S . || true

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== changed files if available =="
git diff --name-only -- docs/design/agenta-mobile/chat-headless-contract.md || true

echo
echo "== staged/unstaged diff around target =="
git diff -- docs/design/agenta-mobile/chat-headless-contract.md | sed -n '1,220p' || true

echo
echo "== locate markdown by exact filename variants =="
fd -a 'chat-headless-contract\.md$' . || true

Repository: Agenta-AI/agenta

Length of output: 350


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target document relevant sections =="
sed -n '80,112p' docs/design/agenta-mobile/chat-headless-contract.md | cat -n

echo
echo "== WP3 plan approval hook sections =="
sed -n '680,705p' docs/design/agenta-mobile/plans/2026-07-12-wp3a-chat-headless-core.md | cat -n

echo
echo "== WP3 plan approval registry section =="
sed -n '705,740p' docs/design/agenta-mobile/plans/2026-07-12-wp3a-chat-headless-core.md | cat -n

echo
echo "== README summary references =="
sed -n '120,135p' docs/design/agenta-mobile/README.md | cat -n

Repository: Agenta-AI/agenta

Length of output: 11550


Remove renderer from the useApprovalDock contract.

useApprovalDock is documented to return only current, count, respond, and approveAll; approval body rendering is resolved through the approval registry and approved copy overrides. Return the registry-entry values from resolveApprovalBody, or update the contract and WP3a task contract consistently before implementation.

```

## 4. Skin slot contract (Layer 3)

Every slot receives data + callbacks only — no antd/x types. Registry keys are the existing
ones: `renderKindFor(...)` → client-tool widget; tool name → approval body;
`resolveToolDisplay(rawName)` → label/source/kind; expand keys from `expandState.ts`.

| Slot | Props (from behavior layer) |
|---|---|
| `MessageBubble` | `{role, variant, avatar, children, isError}` |
| `TextPart` | `{markdown}` |
| `ReasoningPart` | `{text, streaming, expanded, onToggle}` |
| `FilePart` | `{name, kind: FileKind, url, mediaType}` |
| `SourcesList` | `{sources: {url, title?}[]}` |
| `ToolActivityGroup` | `{parts, mode: "summary"\|"live"\|"detailed", summaryLabel, failedCount, expanded, onToggle, onViewTrace?}` |
| `ToolRow` | `{name, displayLabel, source?, status, midText, io?, expanded, onToggle}` |
| `ApprovalCard` | `{current, count, headline?, approveLabel?, Body?, onApprove, onDeny, onApproveAll, onViewTrace?}` |
| approvals registry entry | tool name → `{Body(input, entityId, fallback), headline?, approveLabel?}` |
| clientTool registry entry | `render.kind` → toolName → widget `{meta, settle, degradedEarlierInTurn}` |
| Elicitation fields | per schema kind (string/number/enum/date/boolean/array); engine parses, skin draws |
| `ErrorPart` | `{text, expanded, onToggle}` |
| `NoResponseNotice` | `{}` |
| `QueuedChip` / `QueuedList` | `{queued, onRemove, onClear}` |
| `Composer` | `{onSubmit(text), disabled, streaming, onStop, placeholder, initialMarkdown, onChange, onPasteFile, prefix, header, trailing}` |
| `AttachmentTray` | `{files, rejections, limits, onAdd, onRemove, onDismissRejections}` |
| `EmptyState` | `{entityId, onStart, firstRunPrompt?, canStart, onPrefill?}` |
| Skeletons | transcript / composer / conversation |
| `MessageToolbar` | `{actions: {key, label, icon, onClick}[]}` |
| `JumpToLatestPill` (desktop) | `{visible, onClick}` |
| `WorkingIndicator`, `MessageTimestamp`, `TraceMetrics` | `{}` / `{createdAt}` / `{traceId?, usage?}` |
| `DropOverlay`, `DockContainer` | layout slots |

## 5. WP3a extraction order

1. Neutralize the four type leaks (§2) — behavior-neutral, OSS keeps working.
2. Lift the pure blocks (turn render model, status/error derivation, tool summarization,
approval extraction, hydration) into `@agenta/chat`; OSS re-imports them immediately
(before/after fixture tests prove identical output).
3. Assemble `useAgentConversation` from the lifted blocks + engine (mobile-first consumer).
4. Generalize the three registries so skins register values against shared keys.
5. Desktop re-plumb of the remaining inline host (scroll opt-in, JSX assembly) = follow-up
track, and the contract's acceptance test.
Loading
Loading