Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ffb1625
feat(mobile): steer-lite deny-with-redirect behind the steer flag
ardaerzin Jul 30, 2026
0412de9
feat(web): hand a mobile-initiated OAuth callback back to /m
ardaerzin Jul 31, 2026
c074b48
feat(mobile): sign-in parity — email OTP, social providers, and org SSO
ardaerzin Jul 31, 2026
7ccbf0a
docs(sessions): plan the single source of truth for desktop and mobil…
ardaerzin Jul 31, 2026
38f9605
fix(chat): restore the message store's OSS semantics on the shared lo…
ardaerzin Aug 1, 2026
480ce0f
fix(mobile): derive the session liveness badge through the shared str…
ardaerzin Aug 1, 2026
a2aa64b
fix(mobile): adopt the server transcript through the shared record wa…
ardaerzin Aug 1, 2026
185c875
feat(mobile): brand sign-in, pin every header, and make the workspace…
ardaerzin Aug 2, 2026
34efae6
docs(mobile): plan the split into a reviewable PR stack
ardaerzin Aug 2, 2026
21f1502
fix(chat): re-sync the replay adapter with the OSS original, and rend…
ardaerzin Aug 3, 2026
af8c524
docs(mobile): correct the PR-split plan to the mechanic actually used
ardaerzin Aug 3, 2026
c6ee3c3
feat(mobile): poll the session list head so new sessions appear
ardaerzin Aug 3, 2026
967e79d
feat(mobile): give session and turn statuses a real tag
ardaerzin Aug 3, 2026
df9e3ee
fix(mobile): stop live and ended sessions rendering as the same pill
ardaerzin Aug 3, 2026
804e82f
feat(mobile): make the waiting count a filter instead of a statistic
ardaerzin Aug 3, 2026
ce9b8b0
fix(mobile): make the waiting filter one chip instead of a dangling p…
ardaerzin Aug 3, 2026
64c5706
fix(mobile): stop the list header eating the first screen
ardaerzin Aug 3, 2026
e05c1d7
fix(mobile): pin the workspace selector to the picker header
ardaerzin Aug 3, 2026
b5476ae
docs(mobile): reconcile the design docs with what shipped
ardaerzin Aug 3, 2026
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
8 changes: 8 additions & 0 deletions api/oss/src/tasks/asyncio/sessions/interactions_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ def compose_approval_messages(
warm-parked sandbox still fingerprint-matches and resumes live). An optional
deny-with-redirect ``message`` is appended as a trailing user message, which the
fingerprint's prior-conversation slice excludes.

Where that note is DELIVERED is asymmetric, and verified live (2026-07-30). A warm resume
answers the parked harness gate on the still-pending original prompt and sends no new
prompt (``run-turn.ts``: the resume branch reuses the parked ``promptPromise``), so the
note never reaches the model — only a cold replay closes the replayed transcript with it.
Fingerprint parity therefore makes the note undeliverable in the common case; mobile's
steer control stays flag-gated off until the runner can carry a redirect in-band with the
denial (#5444). The note is still persisted as a user record either way.
"""
messages = build_wire_messages(records)
gated_id = resolve_gated_tool_call_id(records, interaction, answer)
Expand Down
29 changes: 15 additions & 14 deletions docs/design/agenta-mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ headless chat core shared between desktop and mobile skins).
| [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) — **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
finalized sessions surface.
Remaining wave-2 plans (WP2 auth + project drawer, WP3b mobile chat skin, WP4 product pages) are
**deliberately unwritten** — they must be planned against the real wave-1 code and the finalized
sessions surface. WP5 is no longer among them: its plan was written and executed (row above).

## Execution state

Expand All @@ -65,10 +65,11 @@ 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).
Opt-in in dev too, from the repo root (swap `--oss` for `--ee` to match the loaded env file):
`bash ./hosting/docker-compose/run.sh --oss --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.

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

Expand Down Expand Up @@ -179,7 +180,8 @@ breakdown and grounding facts.
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
- Combined review verdict across the five commits above plus the T7 documentation commit:
**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
Expand Down Expand Up @@ -262,7 +264,7 @@ navigation / flows / logic"). All raw-UI; the radix-primitives track re-skins la
cd web && pnpm install
pnpm --filter @agenta/mobile lint # bans + tokens:check
pnpm --filter @agenta/mobile types:check
pnpm build-mobile && test -f web/../web/mobile/.next/standalone/mobile/server.js
pnpm build-mobile && test -f mobile/.next/standalone/mobile/server.js
pnpm dev-mobile # → http://localhost:3000/m, check light+dark
```

Expand All @@ -282,18 +284,17 @@ pnpm dev-mobile # → http://localhost:3000/m, check light+dark
- **Dev `__env.js`:** the entrypoint mirrors it into the bind-mounted `web/mobile/public/` on
container start (gitignored). Until the dev image is rebuilt with the P5 Dockerfile changes,
`/m/__env.js` 404s — harmless console noise.
- **Chores pending:** `.gitignore` allowlist entries for the `mobile-*` skills (they're tracked
via `git add -f`, matching repo precedent — new files inside them would be invisible to
`git status` until allowlisted); Fern regen so the FE `include_ended` param stops being a
runtime cast.
- **Chores:** the `.gitignore` allowlist entries for the `mobile-*` skills are **done** (see
`.gitignore` — both the `.agents/skills/` and `.claude/skills/` blocks). Still pending: Fern
regen so the FE `include_ended` param stops being a runtime cast.
- **Mobile image CI + publication runbook** (WP1 infra tail, EXECUTED —
[plans/2026-07-25-wp1-infra-tail.md](./plans/2026-07-25-wp1-infra-tail.md)):
`.github/workflows/17-check-mobile.yml` build-verifies + smoke-tests the `agenta-web-mobile`
image (both arches) on mobile-path PRs and typechecks `@agenta/mobile`; lint (`turbo run lint`
in workflow 11) and `@agenta/chat` unit tests (recursive package discovery in workflow 12)
were already covered by their generic mechanisms — verified, not changed. **First publication
ordering:** merge → run `17 - check mobile` via `workflow_dispatch` with `push=true,
push_latest=true` → only then can operators pass `run.sh --gh --with-mobile`. Until that first
push_latest=true` → only then can operators pass `--with-mobile` to `bash ./hosting/docker-compose/run.sh --oss --gh` (or `--ee`). Until that first
dispatch-push, the `with-web-mobile` compose profile stays opt-in on purpose — `docker compose
up`/`pull` would fail the whole stack against a `ghcr.io/agenta-ai/agenta-web-mobile:latest`
that doesn't exist yet.
Expand Down
2 changes: 1 addition & 1 deletion docs/design/agenta-mobile/chat-headless-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ useAgentConversation({entityId, sessionId}): {
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}
useApprovalDock({messages, respond}): {open, current, count, responding, respond, approveAll}
useClientToolDispatch()
useAgentChatQueue(...) // exists — moves in
useAgentModelKeyStatus(...) // exists — moves in
Expand Down
17 changes: 14 additions & 3 deletions docs/design/agenta-mobile/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ provider stack before deciding).
- Escape hatch both ways: a `agenta-mobile-optout` / `agenta-mobile-optin` cookie set by
"View desktop site" / "Open mobile version" links; middleware honors it. This also fixes the
current banner's dismissal-not-persisted annoyance.
- `NoMobilePageWrapper` is retired once the gate ships.
- `NoMobilePageWrapper` is retired when the gate is actually turned **on**, not when its code
ships. WP5 landed the gate default-off (`AGENTA_MOBILE_GATE`), so retiring the wrapper any
earlier would leave a mobile visitor with neither the gate nor the blocker. Retirement (T8)
belongs to the deployment window that flips the flag, once WP2 and WP4 can serve what the
redirect points at.
Comment on lines +114 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Separate T8 from the WP5 dependency row.

Lines 114-118 correctly require WP2, WP4, and an enabled AGENTA_MOBILE_GATE. Line 384 still includes retire NoMobilePageWrapper in WP5 with only WP1 as a dependency. A reader can apply T8 while the gate is off and remove the only mobile blocker. Update the table to list gate delivery and T8 retirement as separate milestones.


**URL scheme (mobile app, under basePath `/m`):**

Expand Down Expand Up @@ -169,7 +173,14 @@ Mobile consumes this as-is: project-wide list = `querySessions` without a `refer
per-agent filter = the existing `references` filter; continue-session = resolve the session's
latest turn (turns endpoints exist) → references → hydrate that revision.

### Residual gaps (the revised WP0 scope)
### Residual gaps (the revised WP0 scope) — CLOSED, kept as the record of what was scoped

> **Status (2026-07-25): all five items below are implemented.** They are left here as the gap
> analysis that produced WP0, not as open work. Ordering rides `coalesce(updated_at, created_at)`
> with a direction-matched id tiebreak, search is an escaped `ilike` on the stream name,
> references are batch-hydrated by `latest_turn_per_session` (one `DISTINCT ON` query, not
> per-session), and the zod wire test exists. Only the optional Fern regen for `include_ended`
> is still outstanding. See the README's "WP0 residual" section for the commit-by-commit record.

1. **`updated_at` ordering/cursor** — the query DAO windows on `id` (uuid7 ≈ creation order);
the FE sorts client-side per page, which breaks "last-activity" ordering across pages for
Expand Down Expand Up @@ -364,7 +375,7 @@ same standard, per the repo's instruction-organization model:

| WP | Scope | Depends on |
|----|-------|------------|
| WP0 | BE residual (revised 2026-07-18 — list/title/linkage landed via sessions-extensions): `updated_at` windowing, title search, references echo on list rows, `querySessions` zod test | — |
| WP0 | **EXECUTED.** BE residual (revised 2026-07-18 — list/title/linkage landed via sessions-extensions): `updated_at` windowing, title search, references echo on list rows, `querySessions` zod test | — |
| WP1 | Foundation: skill/instruction infrastructure, `web/mobile` scaffold, shadcn + token bridge + motion presets, lint import-bans, compose/Traefik/`__env.js` | — |
| WP2 | Auth: mobile sign-in (headless SuperTokens + shadcn), callback, context resolution, project drawer | WP1 |
| WP3a | `@agenta/chat` headless core: lift the behavior blocks (host hook, turn render model, hydration, approvals, attachments neutralization), generalize the registries, fixture tests; OSS re-imports the lifted pure blocks (behavior-neutral) | — |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,15 @@ scroll. `session_streams.updated_at` is heartbeat-fed last activity.
"""apply_windowing must support `updated_at` as the order/cursor attribute.

The sessions list is ordered by last activity (`updated_at` is heartbeat-fed on
session_streams). Both ORDER BY and the keyset cursor filters must ride updated_at —
ordering by updated_at while cursor-filtering on another column paginates incorrectly.
session_streams). Both ORDER BY and the keyset cursor filters must ride the SAME
expression — ordering by one column while cursor-filtering on another paginates
incorrectly.

That expression is `coalesce(updated_at, created_at)`, not bare `updated_at`:
`updated_at` is nullable, and a DESC sort puts NULLs first in Postgres, so a session
that never got a heartbeat would sit above every active one. The full statement
therefore mentions `created_at` by design — assert on the coalesced expression, not on
the absence of that column.
Comment on lines +92 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the coalesced expression the implementation and test contract.

The new text requires coalesce(updated_at, created_at) for ordering and cursor predicates. The implementation steps still register raw DBE.updated_at, the sample assertions still require bare updated_at and forbid created_at, and the no-windowing fallback still uses raw updated_at. Update all references to use the same coalesced expression.

Also applies to: 149-156

"""

from datetime import datetime, timezone
Expand Down Expand Up @@ -196,10 +203,12 @@ the references *filter*; extend it to hydrate.

**Files**
- Modify: `api/oss/src/core/sessions/service.py` (`query_sessions` — after fetching streams,
batch-fetch the latest turn per session via `SessionTurnsDAO` (one query,
`DISTINCT ON (session_id) ... ORDER BY session_id, turn_index DESC` or the DAO's existing
latest-turn helper from the turn-index fix `9613e7964e`) and attach `references` (+
`trace_id` if cheap) to each row)
batch-fetch the latest turn per session via `SessionTurnsDAO` and attach `references` (+
`trace_id` if cheap) to each row). This needs a NEW batch helper —
`latest_turn_per_session(session_ids)`, one `DISTINCT ON (session_id) ... ORDER BY
session_id, turn_index DESC` query. The existing latest-turn helper from the turn-index fix
`9613e7964e` takes a single session and would make `/sessions/query` an N+1 path; do not use
it here. Keep the one-call assertion in the service test.
- Modify: response model — either add `references`/`latest_turn` to the session row model the
root query returns, or wrap rows in an enriched envelope; follow whichever the track's
maintainer style suggests (read `SessionsResponse` in `api/oss/src/apis/fastapi/sessions/models.py` first)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Stand up the foundation for the Agenta mobile web app per `docs/design/agenta-mo
**Tech Stack**
Next.js `15.5.18` (workspace pin, enforced by the `next@<15.5.18 → >=15.5.18` pnpm override in `web/package.json`), React `^19`, TypeScript `^5.9`, Tailwind CSS v4 (`@tailwindcss/postcss`, CSS-first config — the latest toolchain shadcn supports), shadcn/ui (registry workflow, `new-york` style, CSS variables), `motion` `^12` (same major as OSS), ESLint 9 flat config + Prettier (repo `web/.prettierrc` applies by upward resolution — no new prettier config), pnpm `11.1.2` + turbo `2.8.20`, Docker Compose + Traefik v2.

**Conventions for all commit steps:** run `git branch --show-current` first — if it prints `gitbutler/workspace`, this repo is in GitButler workspace mode and you must use `but branch new <lane>` / `but commit <lane> -m "..."` per root `AGENTS.md`; the commands below assume plain git on a feature branch (e.g. `mobile/wp1-foundation`). Never include Claude/Anthropic/Co-Authored-By lines in commit messages. All commands run from the repo root unless a `cd` is shown.
**Conventions for all commit steps:** run `git branch --show-current` first — if it prints `gitbutler/workspace`, this repo is in GitButler workspace mode and you must use `but branch new <lane>` / `but commit <lane> -m "..."` per root `AGENTS.md`; the commands below assume plain git on a feature branch (e.g. `mobile/wp1-foundation`). Never include Claude/Anthropic/Co-Authored-By lines in commit messages. All commands run from the repo root unless a `cd` is shown. Before every commit step that touches frontend files, run `cd web && pnpm lint-fix` (the repo-wide convention in `web/AGENTS.md`) — a package-local ESLint run is not a substitute, because the format check in CI runs over the whole `web` tree.

---

Expand Down Expand Up @@ -355,8 +355,9 @@ const {sharedAxisPush, sheetSlideUp, crossfade, reduced} = useMotionPresets()
"public/**",
"next.config.ts",
"postcss.config.mjs",
"tsconfig.json",
"components.json"
"$TURBO_DEFAULT$",
"!tests/**",
"!**/*.md"
],
"outputs": [".next/**", "!.next/cache/**"],
"env": ["NODE_ENV", "NEXT_PUBLIC_*"]
Expand Down Expand Up @@ -786,11 +787,15 @@ const {sharedAxisPush, sheetSlideUp, crossfade, reduced} = useMotionPresets()
"muted-foreground": [color(p.text.secondary.light), color(p.text.secondary.dark)],
accent: [
color(p.surface.controlItemBgActive.light),
color(p.surface.controlItemBgActive.dark),
// Review delta: the palette's dark controlItemBgActive reads olive against the
// mobile surface; zinc[2] is the neutral the dark theme actually wants.
color(p.scales.zinc[2].dark),
],
"accent-foreground": [color(p.text.primary.light), color(p.text.primary.dark)],
destructive: [color(p.semantic.error.light), color(p.semantic.error.dark)],
"destructive-foreground": [color(p.surface.white.light), color(p.surface.white.dark)],
// Review delta: pure white on the dark destructive fill was too hot; the antd dark
// button primary colour is the value the rest of the dark theme uses.
"destructive-foreground": [color(p.surface.white.light), p.componentsDark.Button.primaryColor],
border: [color(p.border.secondary.light), color(p.border.secondary.dark)],
input: [color(p.border.default.light), color(p.border.default.dark)],
ring: [color(p.accent.primary.light), color(p.accent.primary.dark)],
Expand Down Expand Up @@ -932,7 +937,7 @@ const {sharedAxisPush, sheetSlideUp, crossfade, reduced} = useMotionPresets()
cd web/mobile && pnpm dlx shadcn@latest add button skeleton
```
Expected: CLI reports it found `components.json`, and creates `src/components/ui/button.tsx` and `src/components/ui/skeleton.tsx`.
- [ ] Verify: `ls web/mobile/src/components/ui/` → `button.tsx skeleton.tsx`; `git diff web/mobile/package.json` shows the added radix dep; `web/pnpm-lock.yaml` updated.
- [ ] Verify (still inside `web/mobile` from the previous step): `ls src/components/ui/` → `button.tsx skeleton.tsx`; `git diff package.json` shows the added radix dep; `git diff ../pnpm-lock.yaml` shows the lockfile updated.

### Task 3.9 — Commit the design-system foundation

Expand Down
Loading
Loading