[feat] Mobile and package extraction, as one lane - #6065
Conversation
`--ant-lime-6` is a raw antd palette variable that only exists where antd's CSS-var theme is injected. The mark means "it worked", so it takes the semantic role instead: `--ag-colorSuccess`, which the shared token layer defines for both themes and mobile imports.
…nded `HeightCollapse` clips its children rather than unmounting them, so whatever a host passes as `expandedContent` mounts on the first paint of Home — desktop's slot is a lazily-imported tremor dashboard with its own observability query, downloaded and fired before anyone clicks Expand, on the page whose whole point is first paint. Mount it on the first expand and keep it mounted after that. Keeping it beats unmounting on collapse: the collapse animates height to 0 with the content still in place, where dropping the node would animate an empty box. The prop stays a ReactNode — building the element is free, it is the mount that costs — so both callers (Home and the usage summary's strip variant) are fixed as they stand.
The tabbed workspace decides live-vs-replay on entityId alone, which is null while the agent query is still pending — so an agent-backed session rendered the replay branch and its "no agent to message yet" notice until resolution landed. Hold the chat slot on ChatLoading until useAgentEntity settles.
`md:hidden` / `hidden md:block` only toggle `display` — both frames were in the returned tree at every width, so React mounted the conversation twice for one session (two session watches, two record polls, two independent streaming states) and the config/sessions pane twice alongside it. Pick the frame in JS instead, from a `useMediaQuery` hook built on `useSyncExternalStore` so the Pages Router's server render has a defined snapshot and hydration corrects it rather than mismatching. Narrow is the server default: it is the mobile app's common case and the cheaper frame to throw away. A mode toggle still keeps the conversation mounted (the narrow frame flips `display` on children that both stay in the tree; the wide frame always renders it in the split's fill). Crossing 48rem does remount it — different trees — and the comment now says so, along with correcting the stale "below lg" note.
The pending-task guard was a boolean that nothing reset, but `sessionId` is in the effect's dependency list — so on a session change the effect re-ran, found the guard already true, and returned before calling `takePendingTask`. The new session's stashed message stayed in the atom: never sent, never surfaced, never recoverable. Store the session the task was sent FOR, so the guard is per-session.
`useAgentEntity` resolves the agent and the revision independently, so an agent that resolves without a runnable revision leaves `agentId` set and `entityId` null — and the replay screen told the user the session had no agent at all. Branch the notice on the resolved agent instead. Also reflows the destructure prettier rejected, which was failing the lint job.
`setCreating(false)` sat on the single no-agent-created path, so a rejected create — or a `router.push` that rejected or was cancelled — left the button spinning with no way out. Move the cleanup into `finally` and treat navigation as its own outcome: the agent exists either way, so a failed open says that instead of reading as a failed create.
… queried one The fallback comes straight off `?agent=` and went unchecked into a query key and a `retrieveWorkflowRevision` call, so any junk value fired a request that could not succeed and left a junk key in the cache. The queried reference is already gated on `isValidUUID`; apply it in the same place so every caller of the hook is covered.
The comment claimed the latch protects every caller — the home button, the composer, the template cards — but it was a ref inside the hook, and each of those surfaces calls `useCreateAgent()` for itself (the classic home mounts three instances at once). It only ever stopped one button from being double-clicked; clicking a template card and then hitting send while the commit was in flight minted two agents. Move the latch to module scope so the documented guarantee is the real one.
…bounce behind AGENTA_MOBILE_REVERSE_GATE
It defined hover only, and on a borderless transparent target that leaves a keyboard user with nothing but the UA outline. Reuse the app's shared `ICON_LINK` treatment so it focuses like every other icon target here.
…joins the components in @agenta/entity-ui Every component on the agent overview was already shared; only the two-column arrangement was still written twice, inline in the desktop page and again in the mobile screen. AgentOverviewLayout takes main/rail slots and owns the geometry, so /m stops rendering a single 768px column at desktop widths and the two surfaces cannot drift. `scroll` distinguishes the hosts: the desktop page is a full-height frame whose columns scroll independently, while mobile's ScreenScaffold already owns the scroller.
…st the frame The components on this page were already shared; the ARRANGEMENT was not, so /m assembled its own sections in its own order and was missing Files and Usage entirely. AgentOverviewBody is the whole body — composer slot, both list cards, and the rail in one order (what the agent IS, CARRIES, WILL DO, COST) — rendered by the desktop page and the mobile screen alike. SessionListCard moves to @agenta/sessions-ui with the three app verbs as props (mobile's useSessionRowMenu already returns exactly those), replacing mobile's own AgentOverviewSection. AgentFilesCard moves to @agenta/entity-ui/agent, de-antd'd so mobile can render it. The OSS wrappers those two replaced are gone.
…opes its scroll clsx kept both the base gap-10 and the caller's gap-0, so /m's className never won and the mobile overview kept the desktop gap; cn (tailwind-merge) resolves the conflict. The scroll contract is desktop-only — overflow-y-auto was unqualified, so below lg the inner column scrolled instead of the host page, which is what AgentOverview expects to happen.
/m already had a start-session composer on Home; the agent page had none. Rather than a second composer, HomeTaskComposer gains fixedAgentId — with it the picker drops, because an agent's own page already answers which agent and a picker there would let you send from it to a different one. AgentComposer is the mobile binding, on HomeComposer's mint-stash-route flow so attachments staged before the session exists keep their scope.
…s its margins ContentRail renders a plain div, so the body's `flex-1`/`h-full` columns had no definite height to resolve against and the left one scrolled inside a stunted box. Making the wrapper a flex column restores the chain. Padding scales with the viewport — tight on a phone, desktop-like margins from lg.
…sktop margins UsageCard already serves /m's home and takes an appId, so the agent rail scopes it to this agent — the same figures the desktop strip shows. Margins go from 24px to 64px at lg, measured against the desktop page rather than guessed.
The kebab lived in the OSS page: an antd Dropdown wired to app-management modals, with /m offering nothing. AgentActionsMenu is the shared verb set — rename, copy id, copy slug, delete — on Radix, and useAgentActions is its default implementation (updateWorkflow / archiveWorkflow, both already in entities), built the way useSessionActions already does shared verbs. Rename and delete take host overrides so the desktop keeps its app-management modals, which validate and refresh the apps cache; /m has none and falls through to the shared ones. Copying is shared outright. The swap left the OSS header's antd Dropdown, Button, MoreOutlined and copyToClipboard imports dead; they're gone.
The title carried flex-1, so it absorbed the row and pushed the menu to the opposite end. Sizing it to its text puts the two together as on the desktop; min-w-0 keeps a long name truncating.
… shared ones Both were desktop-only. SidebarToggleButton moves to @agenta/navigation-ui — it needed nothing OSS-specific once sidebarCollapsedAtom was in @agenta/navigation and EnhancedButton was antd-free — and takes collapsedAtom as a prop so it does not quietly bypass SidebarShell's own contract. The OSS path stays a re-export because settingsScope imports it too. The version rides buildHelpDocsNavItem's suffix, lazy-loaded from mobile's own package.json.
Help & Docs is a group inside a `vertical` bottom section, so NavMenu draws it through the flyout trigger, not RowLabel — where the suffix had been ported. The label was therefore missing on BOTH the desktop rail and /m. Drawn on both paths now; the collapsed icon rail still omits it, as 111 intended.
… antd items useSessionCardVerbs handed actions.menuItems() straight to SessionListCard's menuFor, which is typed (vm) => SessionMenuEntry[]. antd's MenuProps["items"] admits null entries, groups and submenus, so the shapes do not line up. toSessionMenuEntries is the existing converter the sessions page already used.
…inks to The extraction into `@agenta/sessions-ui` hardcoded `linkScope.agentId` to null, so "View all" and the waiting badge cleared the agent filter on the way out. On /m, whose sessions route is project-wide and narrows by that filter alone, both landed the user on every agent's sessions. The original conditional keyed on whether the host passed a `viewAllHref` at all, which no longer discriminates: both hosts pass one, and what differs is where it points. The desktop's is the agent's OWN sessions route, which re-applies the scope from the route — setting the filter there is invisible (the page hides the chip) but survives to the next project-wide list. So the distinction becomes an explicit `viewAllScopesAgent`, threaded through `AgentOverviewBody`: set on the desktop, absent on /m, and by default the agent travels with the link. While the app id is still resolving, the desktop now falls back to the project sessions route (correct, because the flag then carries the agent as a filter) rather than passing `""` — a link back to the current route, i.e. a click that visibly does nothing. `viewAllHref` is optional for the window where even the project is unknown: "View all" is then not offered and the waiting badge renders as plain text, instead of holding the whole page or offering a dead link.
…gent is known The actions menu was interactive while the agents query was still pending, when `name` is the "Agent" placeholder and the slug is unknown. A rename opened seeded with the placeholder, and archive and delete acted on a record the screen could not yet describe. The menu now waits for the record, which is how the rest of the screen already treats an unresolved agent — no skeleton, since the kebab is last in the header row and nothing shifts when it arrives.
…the attachments `AgentComposer` stashed the pending task and dropped the staged attachments before navigating, then awaited the push without handling it. A push that rejects left the task under a session id the user was never taken to — where it would fire at whoever opened that id later — took the attachments with it, and let the rejection escape the composer's submit handler. The push is now resolved both ways the Pages Router can fail it (`false` on a cancelled route change, a rejection on a real one), and neither leaves residue: the stash comes back out through `dropPendingTaskAtom`, and the attachments go back in the tray. They still have to be cleared BEFORE the navigation — the chat screen's composer restores this session's staged rows from the per-session store, so rows left behind would return as a second copy of what the first turn already carries — which is why `useComposerAttachments` grows the `restoreAttachments` counterpart to `clearAttachments`. It re-stages the entries settled rather than through `addFiles`, which would upload them a second time.
… spine leaves the app The tab model, the static-table helper and the page shell were already pure: no antd, no @/oss. They move verbatim (git mv, so 111's featureFlags->preferences rename keeps its history) and OSS keeps its three import paths as re-exports, so none of the 13 tab files move. Access is injected rather than read: useSettingsAccess needs edition detection, project permissions and org state, none of which exist in a package. The package owns the SettingsAccess shape and the visibility rules; each host computes the flags. A host with no provider gets CLOSED_SETTINGS_ACCESS, so a missing provider hides gated tabs instead of exposing them.
Both were presentation over host state, so both extract cleanly once de-antd'd: Typography -> tokens, Switch -> Radix, Radio -> RadioGroup, Divider -> a rule, danger Button -> variant=destructive, --ant-color-error* -> colorError* tokens. Theme is shared because it is a per-viewer choice both apps honour; the experiment flags stay host-supplied, since a flag only exists where it ships. AccountPage keeps the typed-email gate but delegates the dialog through renderConfirm, so no host can ship a one-click account delete.
First settings extraction with a data half rather than presentation only. The service was 46 lines over axios + the project id, both already in @agenta/shared/api, so it moves with an import swap. OSS re-exports it, keeping one implementation for its three consumers — one of which is in EE, so this is also the first settings piece to cross editions. The APIKeys page itself is untouched; splitting its fetch/permissions/dialogs into a headless hook plus a view is the next unit.
/m's settings was a placeholder saying to use the desktop. It now renders the SHARED page shell and Preferences tab, with the title and description read from the shared tab model so the copy cannot drift from the desktop's. useThemeMode extracts the half of the desktop's ThemeContextProvider both apps need — the stored preference, the theme it resolves to, and the .dark class — keyed off the same storage entry, so a viewer's choice follows them between surfaces. The antd ConfigProvider half stays in the app. Preferences only, deliberately: it is the one tab needing no profile, org or permission state. Access stays CLOSED so nothing edition-gated can leak in before /m can compute real flags.
# Conflicts: # docs/design/sessions-ux-stack/execute-stacked-prs.md # docs/design/sessions-ux-stack/handoff-audit-log-billing.md # docs/design/sessions-ux-stack/plan-settings-nav-takeover.md # docs/design/sessions-ux-stack/plan.md # docs/design/sessions-ux-stack/restack-onto-112.md # web/ee/src/components/pages/settings/Billing/Modals/AutoRenewalCancelModal/index.tsx # web/ee/src/components/pages/settings/Billing/Modals/PricingModal/index.tsx # web/mobile/src/features/agents/AgentComposer.tsx # web/mobile/src/features/agents/AgentOverviewScreen.tsx # web/mobile/src/features/agents/useNewAgentAction.ts # web/mobile/src/features/app/ContextSync.tsx # web/mobile/src/features/auth/useLogout.ts # web/mobile/src/features/chat/ChatScreen.tsx # web/mobile/src/features/chat/Composer.tsx # web/mobile/src/features/chat/LiveConversation.tsx # web/mobile/src/features/chat/SessionWorkspace.tsx # web/mobile/src/features/chat/SessionsPane.tsx # web/mobile/src/features/chat/useAgentEntity.ts # web/mobile/src/features/home/pendingTask.ts # web/mobile/src/features/settings/BillingTab.tsx # web/mobile/src/features/settings/CancelSubscriptionSheet.tsx # web/mobile/src/features/settings/MembersTab.tsx # web/mobile/src/features/settings/PlanChooserSheet.tsx # web/mobile/src/features/settings/ProjectsTab.tsx # web/mobile/src/features/settings/ProviderKeySheet.tsx # web/mobile/src/features/settings/SecretFormSheet.tsx # web/mobile/src/features/settings/SettingsScreen.tsx # web/mobile/src/features/settings/SettingsTabRail.tsx # web/mobile/src/features/settings/WebhookFormSheet.tsx # web/mobile/src/features/settings/WebhooksTab.tsx # web/mobile/src/features/settings/settingsNavScope.tsx # web/mobile/src/features/settings/settingsTabs.ts # web/mobile/src/features/settings/useConfirmSheet.tsx # web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/assets/CommitVariantChangesButton/index.tsx # web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx # web/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsx # web/oss/src/components/TemplateStrip/components/CopiedToast.tsx # web/oss/src/components/pages/overview/agent/AgentOverview.tsx # web/oss/src/components/pages/settings/Projects/index.tsx # web/oss/src/services/tracing/types/index.ts # web/packages/agenta-chat/package.json # web/packages/agenta-chat/src/state/panelLayout.ts # web/packages/agenta-entities/src/organization/types.ts # web/packages/agenta-entities/src/webhook/atoms.ts # web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentOperationsSections.tsx # web/packages/agenta-entity-ui/src/agent/AgentOverviewBody.tsx # web/packages/agenta-entity-ui/src/agent/AgentOverviewLayout.tsx # web/packages/agenta-home-ui/eslint.config.mjs # web/packages/agenta-home-ui/src/HomeTaskComposer.tsx # web/packages/agenta-home-ui/src/UsageCard.tsx # web/packages/agenta-home-ui/src/useCreateAgent.ts # web/packages/agenta-navigation/package.json # web/packages/agenta-navigation/src/dynamic/useSidebarDynamicChildren.ts # web/packages/agenta-observability/eslint.config.mjs # web/packages/agenta-observability/package.json # web/packages/agenta-observability/src/core/presets.ts # web/packages/agenta-observability/src/state/index.ts # web/packages/agenta-playground-ui/src/components/AgentConfigHeader.tsx # web/packages/agenta-playground-ui/src/components/AgentPageHeader/AgentPageHeader.tsx # web/packages/agenta-playground-ui/src/components/CommitVariantChanges/CommitVariantChangesButton.tsx # web/packages/agenta-sessions-ui/src/SessionFiltersBar.tsx # web/packages/agenta-sessions-ui/src/SessionListCard.tsx # web/packages/agenta-sessions-ui/src/SessionTabDragItem.tsx # web/packages/agenta-sessions-ui/src/controls/SessionFilterControls.tsx # web/packages/agenta-sessions/package.json # web/packages/agenta-sessions/src/state/waitingByAgent.ts # web/packages/agenta-settings-ui/eslint.config.mjs # web/packages/agenta-settings-ui/package.json # web/packages/agenta-settings-ui/src/access/DomainsSection.tsx # web/packages/agenta-settings-ui/src/access/SettingToggleRow.tsx # web/packages/agenta-settings-ui/src/access/SsoProvidersSection.tsx # web/packages/agenta-settings-ui/src/audit/AuditEventDrawer.tsx # web/packages/agenta-settings-ui/src/audit/AuditLogPage.tsx # web/packages/agenta-settings-ui/src/billing/PricingPlans.tsx # web/packages/agenta-settings-ui/src/billing/UsageProgressBar.tsx # web/packages/agenta-settings-ui/src/index.ts # web/packages/agenta-settings-ui/src/projects/ProjectsPage.tsx # web/packages/agenta-settings-ui/src/secrets/NamedSecretTable.tsx # web/packages/agenta-settings-ui/src/secrets/SecretProviderTable.tsx # web/packages/agenta-settings-ui/src/tools/ConnectModal.tsx # web/packages/agenta-settings-ui/src/tools/ConnectionsList.tsx # web/packages/agenta-settings-ui/src/tools/IntegrationGrid.tsx # web/packages/agenta-settings/eslint.config.mjs # web/packages/agenta-shared/package.json # web/packages/agenta-ui/src/components/ui/data-table.tsx # web/packages/agenta-ui/src/styles/code-editor-styles.css # web/packages/agenta-ui/src/theme/useThemeMode.ts
# Conflicts: # web/oss/src/components/pages/observability/dashboard/AnalyticsDashboard.tsx
# Conflicts: # web/oss/src/components/EntityIdentity/AgentNameInline.tsx # web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/assets/useCommitHostAdapter.ts
The lane merges brought both persistence implementations for the chat state: `tabStorage`'s subscribe-less storage and the `atomWithStorage` panel-layout atom. `sessionMessages` ended up importing `createJSONStorage` alongside `tabLocalStorage` while using only the latter.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo many files! This PR contains 1319 files, which is 1019 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (1319)
You can disable this status message by setting the |
Measured against this lane the drift branch was two commits, not the 68 it showed
against `fix/package-query-client`: 66 of them had already landed on the sessions
lanes and arrived when those lanes were merged. The docs commit was already present,
so this is the one remaining code commit, 137 files.
Eighteen files conflicted. Neither side was a superset, so they were resolved
individually rather than by preferring a branch:
- `TemplateDetail` — drift adds a `layout` prop (toolbar/rail) but still renders both
shells. Took drift's file and re-applied the single-shell fix on top, so the markdown
body is built once.
- `data-table` and `ProjectsPage` — drift's versions already carry the argument-less
`onReload()` and keyboard-activated rows, so they were taken whole. A partial
resolution had left them referencing `visibleActions` and `rowActions`, which do not
exist on this side.
- `AgentOverviewScreen` — both hunks are one structural change (the shared page-width
cap replacing `ContentRail`), so they had to move together.
- `useSidebarDynamicChildren` — the merged body is drift's, which drops group labels;
keeping our hunk would have orphaned `groupsAreInformative`.
Two regressions the merge introduced, both fixed here:
- `AnalyticsDashboard` went back to `${formatNumber(failure_rate)}%` on a 0-1 fraction,
so a 5% failure rate read as "0.05%". Drift had fixed this and a later merge from the
stale `fix/package-query-client` lineage undid it.
- `gatewayTrigger/state/invalidate.ts` reached for the `queryClient` singleton, which
the host-client rule bans: `/m` installs its own client, so those writes land in an
orphan cache and do nothing. It resolves per call now.
Both come from the same shape — drift predates work that landed after it, so replaying
its history can quietly revert newer fixes.
Verified: `pnpm run lint` 25/25, `tsc --noEmit` clean for oss, ee and mobile.
|
@coderabbitai resolve merge conflicts |
|
❌ Failed to resolve merge conflicts Unexpected error: Request timed out after 1800000ms (requestId=5f6a27dd-f0d0-4189-ad78-7877f93dc1a1) Please resolve conflicts manually. |
Context
Our mobile and package-extraction work has been sitting in 42 stacked PRs (#5865 to #5894, #5915, #5918, #5954 to #5963), all chained base-on-base. Nothing can land that way: reviewing a 42-deep stack is impractical, every lane's CI has been red since the bottom of the stack, and
release/v0.112.0has moved 206 commits underneath us since we forked from it.This collapses all 42 into one lane so the work can be reviewed and landed as a unit.
Changes
lane/mobile-extracted-packagesis based at613368b81b, the commit the first lane (pkg/auth) actually forked from, not today'srelease/v0.112.0. Branching from the current tip would have pulled in 595 files of other people's work and turned 42 clean merges into a conflict resolution exercise. GitHub computes the diff from the merge base, so this PR still shows only our changes.Each lane was merged in dependency order with
--no-ff, so every one keeps its own merge commit and stays attributable:390 commits, 1297 files, 42 merge commits.
39 of the 42 merged with no conflicts. Three needed resolution, and one of those is worth knowing about.
fix/package-query-clientwas never rebased onto its stated baseIts PR (#5915) declares
docs/sessions-ux-stackas the base, but the two branches diverged on 8 August and never reconverged. It carries an independent copy of the stack under different SHAs, which is why merging it produced 88 conflicts, 64 of themadd/add.That matters beyond this PR: the whole observability stack (
obs/wp0throughobs/wp6) descends from that branch, so it inherited the stale copy and silently dropped work that had already landed on the sessions lanes. Merging the lanes directly recovers it. This lane carries 15 filesobs/wp6does not, including the Stripe checkout tab reservation, the workspace permission checks, the Build/Chat layout persistence, the IME composition guard, and the tool-call in-flight flag.Tests
pnpm run lint: 25/25 tasks passtsc --noEmitclean for@agenta/oss,@agenta/eeand@agenta/mobilesessionMessagesimportedcreateJSONStoragealongsidetabLocalStoragewhile using only the latter, left over from the two persistence implementations meeting.The v0.112 drift remediation is in here too
fix/mahmoud-112-driftis merged. Measured against this lane it was two commits, not the 68 it showed againstfix/package-query-client: 66 of them had already landed on the sessions lanes and arrived when those lanes were merged.Eighteen files conflicted and were resolved individually, because neither side was a superset.
TemplateDetailtook drift's newlayoutprop with our single-shell fix re-applied on top;data-tableandProjectsPagewere taken whole from drift, which already carries both of our fixes there.Two regressions the merge introduced were caught and fixed:
AnalyticsDashboardwent back to rendering a 0-1 fraction with a%suffix, so a 5% failure rate read as "0.05%".gatewayTrigger/state/invalidate.tsreached for thequeryClientsingleton, which the host-client rule bans:/minstalls its own client, so those writes land in an orphan cache and do nothing.Both come from the same shape. The drift branch predates work that landed after it, so replaying its history can quietly revert newer fixes.