Skip to content

[feat] Agent-first sidebar for new signups, Playground as app default - #5478

Merged
mmabrouk merged 7 commits into
temp-106.1from
feat/simplify-platform-for-first-signup-users
Jul 28, 2026
Merged

[feat] Agent-first sidebar for new signups, Playground as app default #5478
mmabrouk merged 7 commits into
temp-106.1from
feat/simplify-platform-for-first-signup-users

Conversation

@ashrafchowdury

@ashrafchowdury ashrafchowdury commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Context

New signups land on the full platform sidebar — Prompts, the Evaluation group,
Overview, Registry, Evaluations — which is noise when their only goal is building an
agent. Two changes narrow that first experience: new signups get an agent-first
sidebar, and opening any workflow now lands on its Playground instead of the Overview
page (which is one of the pages the simplified sidebar hides).

This is Phase 1 of the plan in docs/design/simplify-nav-new-users/. The Settings →
Account toggle to switch back to the full view (Phase 2) is not in this PR.

Changes

Agent-first sidebar for new signups

The sidebar now hides the advanced areas for users who signed up under this experience:
Prompts and the Evaluation group at the project level; Overview, Registry, and
Evaluations inside an app. Home, Agents, Observability, and the app Playground always
stay.

New durable per-user key navSimplifiedDefaultAtom
(agenta:onboarding:<userId>:nav-simplified, default false), written only at signup
alongside setIsNewUser(true) in usePostAuthRedirect. The sidebar reads a single
derived atom advancedNavHiddenAtom; the five entries gained an isHidden flag driven
by it (app-scope entries OR it onto their existing condition, so nothing that was hidden
before becomes visible).

It deliberately does not reuse isNewUserAtom. That flag is sticky-true for everyone
who ever signed up, so deriving from it would strip advanced nav from existing users.
The fresh key defaults to false, so existing users are unaffected and keep the full
nav.

advancedNavHiddenAtom is the stable seam for Phase 2: it becomes
override ?? navSimplifiedDefault without any consumer changing.

Workflows open in the Playground

Opening an agent or prompt now lands on its Playground, not Overview. This applies
everywhere a workflow is entered: the /apps/[app_id]/ redirect, the sidebar child
links, row clicks on the Agents and Prompts pages, and the app-switch fallback.

Before: apps redirected to /overview, and only evaluators went to /playground.
After: every workflow kind defaults to /playground.

Tests / notes

  • tsc and ESLint pass clean on OSS and EE.
  • No automated sidebar test: @agenta/oss has no CI-wired vitest runner, so a test in
    oss/src would never run. Covered by tsc + ESLint + manual QA. Standing up an oss
    vitest harness is a possible follow-up.
  • Includes the design workspace under docs/design/simplify-nav-new-users/ (planning
    docs only).

What to QA

  • Sign up a brand-new account. The sidebar shows Home, Agents, Observability at the
    project level and only Playground + Observability inside an app. No empty
    "Evaluation" section header remains.
  • Open an existing account (or flip agenta:onboarding:<userId>:nav-simplified to
    false in localStorage and reload). The full sidebar returns, all pages present.
  • From the Agents or Prompts table, click a row. It opens the Playground, not Overview.
  • Visit /apps/<app_id>/ directly. It redirects to /apps/<app_id>/playground.
  • Regression: open an evaluator workflow. It still lands on the Playground as before.

@vercel

vercel Bot commented Jul 24, 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, Comment Jul 27, 2026 8:58pm

Request Review

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jul 24, 2026
@dosubot dosubot Bot added enhancement New feature or request frontend labels Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 reflects the main change: simplified navigation for new signups and defaulting workflows to Playground.
Description check ✅ Passed The description is directly aligned with the sidebar simplification and Playground-default routing changes in the diff.
✨ 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/simplify-platform-for-first-signup-users

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.

@ashrafchowdury

ashrafchowdury commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

this is phase 1

in phase 2, I suggested having a switch toggle in the settings > profile to go back to the older view to see all the options in the sidebar. This is for older users who would be invited to collaborate with new team members, but they are going to see the new view, so they turn the old view back on.

This PR only contains phase 1, which is clean; phase 2 required alignment, which is why I have not implemented it

@ashrafchowdury
ashrafchowdury changed the base branch from ts-chore/fix-tsc-issues to release/v0.106.0 July 24, 2026 14:25
@ashrafchowdury
ashrafchowdury marked this pull request as ready for review July 24, 2026 14:55
@ashrafchowdury

Copy link
Copy Markdown
Contributor Author
image image

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-0bef.up.railway.app/w
Project agenta-oss-pr-5478
Image tag pr-5478-e08bf02
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-07-27T21:09:37.717Z

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @ashrafchowdury for the pr howver it is right now unreviewable. Not sure which parent branch you used but the diff is huge. Also, let's have part 2 at the same time, otherwise it is very hard to test

@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: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (4)
web/oss/src/components/EvaluationRunsTablePOC/components/filters/EvaluationRunsHeaderFilters.tsx (1)

454-468: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Move the ignored body styles to content.

antd Popover styles.body is not a supported slot for styling the popover panel, so the panel maxWidth, transparent background, shadow, and border are ignored. Move those declarations to content and remove the as PopoverProps["styles"] cast.

web/oss/src/services/organization/api/index.ts (1)

191-200: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Use the Organization Fern accessor for this request.

This changed API call still uses raw Axios. Move it behind a per-resource accessor (adding one in web/packages/agenta-sdk/src/resources.ts if needed) so organization requests use the shared client contract. As per coding guidelines, “Frontend API code must use per-resource Fern client accessors … never raw axios.”

Source: Coding guidelines

web/oss/src/state/newObservability/helpers/index.ts (1)

4-9: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make numeric coercion field-aware and finite.

coerceNumericValue is called on observability filters via valueCodec.ts, and it recursively converts every string value it encounters. That can turn identifier strings such as "00123", "1e17", and "0x10" into different numbers, and non-finite inputs like "Infinity" into null. Keep string identifiers/labels unchanged and restrict numeric coercion to fields that are explicitly numeric.

web/oss/src/lib/hooks/usePreviewEvaluations/index.ts (1)

379-390: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Normalize the testset shape before creating scenarios.

CreateEvaluationRunInput["testset"] is Testset, whose contract does not guarantee data; only the revision branch populates it. The cast at Lines 422-426 merely suppresses TypeScript and non-revision inputs can dereference testset.data as undefined, causing the run to be created without scenarios. Define a hydrated testset contract and populate it on every path, then use the same normalized testcase IDs for step creation.

Also applies to: 420-427, 450-472

🟠 Major comments (21)
web/oss/src/lib/Types.ts-71-81 (1)

71-81: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Model legacy testcase payloads as a real union.

The comment says legacy testcases carry inputs instead of data, but data is required. PreviewTestCase therefore cannot represent the documented legacy payload, and consumers may read data when it is absent at runtime. Make the two shapes explicit or normalize the payload before exposing this type.

web/oss/src/components/Sidebar/components/ListOfProjects.tsx-79-83 (1)

79-83: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fix the organization-to-project fallback contract.

projectsByOrganization drops any project missing both proj.organization_id and a matching default_workspace from the org list, so unowned projects disappear from the sidebar. Ensure /api/projects returns organization_id, or derive the workspace→org mapping from an endpoint that actually returns it instead of casting org list rows to OrgDetails.

web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/types.d.ts-23-24 (1)

23-24: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the optional trace-ID contract with the annotation save path.

The drawer now permits missing IDs, but the save path asserts they are required. Missing IDs can yield an empty payload while reporting success; partial IDs can produce invalid links.

  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/types.d.ts#L23-L24: require IDs for this flow or explicitly model missing-link behavior.
  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/AnnotateDrawerTitle/index.tsx#L131-L131: remove the unsound cast and validate or handle absent IDs before saving.
web/oss/src/components/pages/observability/components/ObservabilityHeader/index.tsx-359-367 (1)

359-367: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Filter invalid stale selections before populating the testset drawer.

TestsetTraceData requires a string key and non-null data, but node?.key can be undefined and getAgData(node ?? undefined) can return null. The cast only hides the mismatch, allowing invalid drawer rows. Skip unresolved nodes/data before calling setTestsetDrawerData.

web/oss/src/hooks/usePostAuthRedirect.ts-139-143 (1)

139-143: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not write the navigation flag through a stale active-user ID.

If both identity lookups fail, Line [118] leaves the previous onboardingStorageUserIdAtom value untouched. The new setters at Lines [142] and [154] can then persist true under that previous user’s nav-simplified key, while the new user remains unsimplified. Gate user-scoped writes on a verified sessionUserId and clear or fail on stale identity state.

Also applies to: 151-155

docs/design/simplify-nav-new-users/plan.md-143-153 (1)

143-153: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Scope the Phase 2 override per user.

"agenta:nav:simplified-override" is a browser-wide key, but the plan promises per-user behavior. One user’s choice will apply to the next account in the same browser. Reuse the existing user-scoped storage family or include the user ID in the atom key.

docs/design/simplify-nav-new-users/plan.md-54-68 (1)

54-68: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent a first-render full-navigation flash.

With atomWithStorage(..., false) and the default getOnInit, nav-simplified renders as false before hydration and only switches to true after the client reads localStorage. Guard the full nav until hydration completes, or use a hydration-safe loading/default value that cannot briefly expose advanced items.

web/ee/src/components/pages/settings/Billing/index.tsx-173-174 (1)

173-174: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle absent usage data instead of casting it away.

Object.entries(usage!) still throws when the usage request fails, while the member bars receive undefined cast as number and can display invalid values. Use an explicit loading/error/empty state and render the users bars only when the corresponding quota data exists.

Also applies to: 204-223

web/ee/src/components/pages/settings/Billing/index.tsx-132-135 (1)

132-135: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the renewal date when subscription loading fails.

The current render exits only while isSubLoading || isUsageLoading, but it still renders when the subscription query remains absent or errors, and subscription?.period_end becomes Invalid Date. Render the date only when subscription?.period_end exists, or surface the subscription error state before this section.

web/oss/src/components/EntityIdentity/useRenameApp.ts-48-57 (1)

48-57: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not cast a nullable projectId to string.

The cast does not change the runtime value, so null can still reach updateWorkflow. Guard or resolve the project scope before issuing the mutation; otherwise valid rename attempts can fail with an invalid API request.

Proposed fix
                 const {projectId} = getProjectValues()
+                if (!projectId) {
+                    throw new Error("Cannot rename an app without a project scope")
+                }
-                await updateWorkflow(projectId as string, {
+                await updateWorkflow(projectId, {
web/oss/src/components/EvalRunDetails/atoms/table/scenarios.ts-274-276 (1)

274-276: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard projectId inside queryFn.

enabled skips automatic fetches, but manual refetching from a disabled atomWithQuery can still run queryFn; add a runtime projectId check here so fetchEvaluationScenarioWindow() does not send /evaluations/scenarios/query with a missing/null project_id.

Source: MCP tools

web/oss/src/components/EvalRunDetails/utils/buildSkeletonColumns.ts-55-80 (1)

55-80: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the output skeleton argument order instead of preserving the runtime bug.

Lines 115-121 call createSkeletonGroupColumns without the required stepType, so stepType becomes 200, startOrder is undefined, and every output column receives order: NaN. Keep startOrder required and pass "invocation" explicitly.

Proposed fix
 const createSkeletonGroupColumns = (
     groupId: string,
     label: string,
     kind: EvaluationTableColumnGroup["kind"],
     columnKind: EvaluationColumnKind,
-    stepType: EvaluationTableColumn["stepType"] | number,
-    startOrder?: number,
+    stepType: EvaluationTableColumn["stepType"],
+    startOrder: number,
 ): {columns: EvaluationTableColumn[]; group: EvaluationTableColumnGroup} => {
     const columns: EvaluationTableColumn[] = []
     for (let index = 0; index < SKELETON_COLUMNS_PER_GROUP; index += 1) {
-        const order = (startOrder as number) + index
+        const order = startOrder + index
...
     createSkeletonGroupColumns(
         "outputs",
         "Model Outputs",
         "invocation",
         "invocation",
+        "invocation",
         200,
     )
web/oss/src/components/EvalRunDetails/atoms/types.ts-6-12 (1)

6-12: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Align ScenarioStepEntry with the keys returned by the batcher.

scenarioSteps.ts stores the result of snakeToCamelCaseKeys(rawStep), so top-level trace_id/testcase_id become traceId/testcaseId. This open Record<string, any> contract can make snake_case reads compile while returning undefined; preserve raw fields or update the type and consumers to the actual camel-cased shape.

web/oss/src/components/EvalRunDetails/atoms/table/run.ts-5-5 (1)

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

Use the generated evaluations API client instead of raw axios.

ensureEvaluatorRevisions() now imports the configured axios instance to patch /evaluations/runs/${runId}, but frontend API calls should use the Fern per-resource accessor instead. EvaluationsClient.editRuns() already exposes this PATCH endpoint via @agenta/sdk/resources; add that accessor there if needed and use it for the edited run body.

Source: Coding guidelines

web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/utils.ts-177-183 (1)

177-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the PromptPreviewAttachment contract.

Lines 179-183 construct an attachment without the required type: "image" field, even though PromptPreviewAttachment requires it. The as unknown as cast only hides the mismatch and can break consumers that inspect the attachment type. Create a validated attachment with the required field, or widen the shared type to match the actual runtime shape.

web/oss/src/components/EvalRunDetails/components/columnVisibility/ColumnVisibilityPopoverContent.tsx-22-22 (1)

22-22: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat "online" as an automatic evaluation type.

Line 22 enables "online", but visibilityStaticMetricMap selects human metrics for every value other than "auto" (Lines 160-163). usePreviewEvaluations maps online evaluations to automatic, so the popover can expose the wrong static metric columns.

Proposed fix
 const metricsForType =
-    evaluationType === "auto"
+    evaluationType === "auto" || evaluationType === "online"
         ? columnData.staticMetricColumns.auto
         : columnData.staticMetricColumns.human
web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/ContextChipList.tsx-30-31 (1)

30-31: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Pass the resolution result, not just the variant ID.

WorkflowReferenceChip only fetches variantReferenceQueryAtomFamily(revisionId ?? variantId), which can load a missing workflow by variant ID when only the revision is cached. Derive the label in InvocationSection and pass that through here, or change the chip to query both IDs and fall back safely; don’t store the label in atom metadata, which keeps raw reference IDs in downstream query keys.

web/oss/src/state/workspace/atoms/mutations.ts-26-27 (1)

26-27: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pass the organization PATCH body as an object.

The cast does not change the runtime value: Axios sends a bare string even though updateOrganization requires {name: string}. The workspace update can succeed while the organization update fails, leaving the rename partially applied.

Proposed fix
-            updateOrganization(organizationId, name as unknown as {name: string}),
+            updateOrganization(organizationId, {name}),
web/oss/src/pages/workspaces/accept.tsx-97-99 (1)

97-99: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate invite identifiers before calling the accept API.

The casts allow absent values through as undefined; acceptWorkspaceInvite then interpolates them directly into the URL and query string, producing incomplete invite URLs unless the handler is changed to intentionally omit optional identifiers. Reject malformed invites before the call, or update acceptWorkspaceInvite to skip optional workspaceId/projectId when they are absent.

web/oss/src/lib/hooks/useEvaluationRunMetrics/index.ts-80-84 (1)

80-84: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Perform the snake_case-to-camelCase conversion before claiming Metric[].

Metric is SnakeToCamelCaseKeys<MetricResponse>, but this identity map preserves the raw snake_case keys. Downstream consumers will read camelCase fields that do not exist. Use the project’s actual conversion helper, or expose MetricResponse until conversion is implemented.

web/oss/src/services/runMetrics/api/index.ts-691-693 (1)

691-693: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not cast incomplete stats to MetricDistribution.

This branch returns raw stats without the required distribution and binSize fields, despite the declared return type requiring both. Return undefined when distribution data is unavailable, or construct a complete MetricDistribution before returning.

🟡 Minor comments (18)
web/oss/src/components/pages/evaluations/cellRenderers/cellRenderers.tsx-177-180 (1)

177-180: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Map EvaluationStatus.FAILED explicitly.

The enum defines FAILED as "failed", but the map omits it. Such evaluations fall through to "Unknown" instead of displaying the failure state. Add the same failure label/color used by EvaluationStatus.FAILURE.

web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/SelectEvaluators/index.tsx-75-78 (1)

75-78: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not collapse missing slugs to the empty-string sentinel.

All evaluators without a slug share "", while the checkbox still receives a nullish value. A selection containing "" can mark or disable every slug-less evaluator, and clicks can add an invalid value to selectedEvaluators. Filter or disable evaluators without a real slug before rendering.

web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx-254-256 (1)

254-256: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Materialize a unique row key before handing traces to the virtual table.

Raw TraceSpanNode values do not guarantee key, but the new type and cast assert that they do; the empty-string fallback then collapses rows lacking both identifiers into duplicate table keys.

  • web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx#L254-L256: normalize traces into rows with a guaranteed stable key instead of casting the array.
  • web/oss/src/components/pages/observability/assets/getObservabilityColumns.tsx#L32-L37: align TraceRow with the normalized runtime shape.
  • web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx#L290-L293: remove the "" fallback and rely on the normalized unique key.
web/oss/src/components/pages/evaluations/utils.ts-87-93 (1)

87-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include rawVariantId in the null-result guard.

The guard at Line 113 still returns null when the invocation contains only a variant ID, discarding the new value extracted here. Add !rawVariantId to that condition.

Suggested fix
-    if (!rawAppId && !rawRevisionId && !rawVariantName) return null
+    if (!rawAppId && !rawRevisionId && !rawVariantId && !rawVariantName) return null
web/oss/src/components/TestsetsTable/hooks/useTestsetsColumns.tsx-114-115 (1)

114-115: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Copy the row’s actual ID.

TestsetTableRow uses id, not _id; therefore this expression usually becomes "", and copyToClipboard returns without copying. Use String(record.id ?? "") instead.

Proposed fix
-                                            copyToClipboard(String(record._id ?? ""))
+                                            copyToClipboard(String(record.id ?? ""))
web/oss/src/components/TestsetsTable/components/TestsetsHeaderFilters.tsx-49-58 (1)

49-58: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply the popover overrides through an antd v6-supported styles key.

Popover.styles.body is not a Popover slot, and the cast hides the invalid key so the max-width/visual overrides are ignored. Move the maxWidth and visual overrides into styles.root (the overlay) and styles.container/styles.content as appropriate for antd 6.1.3.

web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceSidePanel/TraceReferences/index.tsx-88-88 (1)

88-88: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard missing testset IDs before rendering TestsetTag.

The filter above accepts slug-only references, so id as string can still be undefined at runtime and produce a broken testset link. Require id for this branch or use a slug-capable component.

web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx-203-204 (1)

203-204: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a supported Popover semantic slot instead of body.

Ant Design’s Popover classNames/styles use root, container, content, title, and arrow; body is not a documented slot, so this override may be ignored. For the panel padding/dimension changes targeted here, use container (and content/root if needed) instead.

Source: MCP tools

docs/design/simplify-nav-new-users/README.md-3-4 (1)

3-4: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Refresh the planning status.

This README says the work is “ready to implement,” while status.md and the implementation state that Phase 1 is already implemented. Mark this document as implemented or clearly label it as a historical plan.

docs/design/simplify-nav-new-users/README.md-15-18 (1)

15-18: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the documented Phase 1 scope with the shipped behavior.

Both documents describe a sidebar-only change with no in-app link changes, but this PR also redirects prompt/agent entrypoints and dynamic sidebar children to /playground.

  • docs/design/simplify-nav-new-users/README.md#L15-L18: update the design scope to include the route-entry changes.
  • docs/design/simplify-nav-new-users/status.md#L14-L17: update the implementation status to reflect the broader behavior.
docs/design/simplify-nav-new-users/research.md-3-24 (1)

3-24: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale sidebar line references.

The documented targets no longer match the implementation: Prompts is Line [78], Evaluation is Line [95], Overview is Line [155], Registry is Line [172], and Evaluations is Line [182] in the supplied sidebar file, not the listed lines. Update these references or use symbol/key references instead.

docs/design/simplify-nav-new-users/plan.md-41-47 (1)

41-47: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the plan with the delivered test coverage.

Slice 0 remains a required Phase 1 exit criterion, but the PR objectives state that automated sidebar tests are not included. Mark this slice deferred or replace its exit criterion with the documented manual QA, and track test-runner enablement separately.

web/oss/src/components/EvalRunDetails/atoms/metricProcessor.ts-696-696 (1)

696-696: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the metric ID type guard validate strings.

Boolean(id) accepts truthy non-string values, and runMetrics are typed as any, so newMetricIds can include numeric/boolean/object IDs despite being inferred as string[]. Narrow with typeof id === "string" before accepting the ID.

web/oss/src/components/EditorViews/SimpleSharedEditor/index.tsx-57-60 (1)

57-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Honor an explicit editorProps.language.

The state now accepts arbitrary CodeLanguage values, but initialization ignores props.editorProps?.language and the fallback effect forces "markdown" for every non-JSON/YAML/HTML editor. A TypeScript or Python editor can therefore show the wrong selected format. Seed from the configured language and only apply the Markdown default when none is provided.

Proposed fix
-    const [language, setLanguage] = useState<Format | CodeLanguage>(() =>
-        isJSON ? "json" : isYAML ? "yaml" : "text",
+    const [language, setLanguage] = useState<Format | CodeLanguage>(() =>
+        isJSON ? "json" : isYAML ? "yaml" : props.editorProps?.language ?? "text",
     )
...
-        } else {
+        } else if (!props.editorProps?.language) {
             setLanguage("markdown")
         }

Also applies to: 80-93

web/oss/src/components/EnhancedUIs/Drawer/index.tsx-20-28 (1)

20-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve resolver-based drawer styles when applying width.

Ant Design v6 supports styles as a resolver function; when it is provided with a non-zero width, this code replaces it with an empty wrapper: { width }, discarding user styles. Compose the resolver instead and merge the wrapper width into the resolved styles.

web/oss/src/lib/hooks/useEvaluationRunMetrics/index.ts-42-44 (1)

42-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not append an empty run_ids value.

When runIds is [], the cast does not change the runtime value; URLSearchParams serializes it as run_ids=. If a scenario ID is also present, the later SWR-key check still sends that empty parameter. Branch on typeof runIds === "string" and skip empty arrays.

web/tsc-error-inventory.md-3-13 (1)

3-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the inventory dates and phase labels.

The file says it was generated on July 22, 2026, but labels Wave 2 as final on July 24, 2026 and separately calls 105 OSS plus 11 EE-only errors the “Final-tail inventory.” Regenerate the report after the final wave or label these figures explicitly as intermediate/forecast values.

web/packages/agenta-ui/src/InfiniteVirtualTable/features/InfiniteVirtualTableFeatureShell.tsx-142-146 (1)

142-146: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Forward locale.emptyText or remove the prop until implemented.

This API accepts custom empty text but explicitly drops it before rendering, so callers get a silent no-op. Forward it to the inner table and test it, or keep the option out of the public contract until it works.

🧹 Nitpick comments (10)
web/oss/src/components/Playground/Playground.tsx (1)

115-124: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Extract a stable, typed provider adapter.

The inline ChatTurnAssistantActions component is recreated on every Playground render. If provider entries are consumed as component types, this can remount the assistant subtree and reset local state. Moving it to module scope also gives the unknown double cast a single, reviewable contract boundary.

As per coding guidelines, frontend code should minimize React re-renders and avoid unstable inline functions and objects.

Source: Coding guidelines

web/oss/src/components/Playground/Components/TestsetDropdown/index.tsx (1)

177-180: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Resolve the "success" status contract at its source.

Both predicates cast r.status to string while stating that MessageExecutionStatus never emits "success". Remove the dead branch if that is guaranteed; otherwise widen or normalize the producer type and add coverage. The local casts currently hide a contract mismatch from other consumers.

Also applies to: 237-240

web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceSidePanel/TraceLinkedSpans/index.tsx (1)

57-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not describe link.trace as dead.

linksAndReferencesAtom adds trace to returned links, so this access can be live. Add the optional field to TraceDrawerSpanLink and read it type-safely, or remove the access intentionally; the any cast currently hides this cross-file contract.

web/oss/src/components/SharedDrawers/AddToTestsetDrawer/atoms/drawerState.ts (1)

122-123: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Align TestsetTraceData with the actual JSON shape instead of casting it.

extractAgData can contain nested arbitrary JSON, but TestsetTraceData["data"] is declared as KeyValuePair. The cast only suppresses TypeScript and leaves downstream consumers with an inaccurate contract; widen the shared data type and originalData accordingly.

Also applies to: 158-158

web/oss/src/components/pages/overview/variants/VariantPopover.tsx (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial

Run the required frontend lint fix before commit.

Run pnpm lint-fix from web and fix all reported errors before committing these frontend changes.

Source: Coding guidelines

web/oss/src/components/SharedDrawers/SessionDrawer/store/sessionDrawerStore.ts (1)

138-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Align the OSS/entities trace contract once instead of repeating unknown as casts.

These sites all bypass assignability checks at the same package boundary. Introduce a shared trace type or one adapter/validator, then consume that contract throughout the trace drawer and Playground flow.

  • web/oss/src/components/SharedDrawers/SessionDrawer/store/sessionDrawerStore.ts#L138-L147: type the transformation result through the shared contract.
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/OverviewTabItem/index.tsx#L36-L38: remove the local EntityTraceSpan double-cast.
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/OverviewTabItem/index.tsx#L50-L62: consume the shared typed span in drill-in reads.
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/TraceTypeHeader/index.tsx#L96-L102: pass the shared type to replay eligibility helpers.
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceHeader/index.tsx#L191-L204: type all relative-navigation transformation branches centrally.
  • web/oss/src/components/SharedDrawers/TraceDrawer/store/openInPlayground.ts#L22-L27: pass the shared trace contract to openFromTrace.
web/oss/src/components/SharedDrawers/TraceDrawer/components/AccordionTreePanel.tsx (1)

268-269: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not cast GlobalToken to a complete JSSTheme.

JSSTheme also requires isDark and fontWeightMedium; this cast does not provide them at runtime. Narrow the style callback to the tokens it uses, or construct an adapter that supplies the complete theme contract.

web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/AnnotationTabItem/index.tsx (1)

193-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the enriched annotation instead of casting through any.

mergedAnnWithEvaluator adds evaluator to each item, so this is not dead access. Type the memo/grouping result as an enriched AnnotationDto and read annotations?.[0]?.evaluator directly; this also removes the misleading two-line comment.

As per coding guidelines, in-code comments should be at most one short line unless documenting a genuinely surprising constraint.

Source: Coding guidelines

web/oss/src/components/EvalRunDetails/components/FocusDrawer.tsx (1)

144-145: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Replace the nullable runId cast with a real contract.

useFocusDrawerSections explicitly accepts string | null, while usePreviewTableData requires string and uses the value to create run-scoped atom families. (runId ?? undefined) as string only hides the mismatch; it does not make a null key safe. Widen the hook contract and explicitly handle the no-run case, or prove this caller cannot receive null.

web/oss/src/state/newObservability/atoms/queryHelpers.ts (1)

367-376: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Verify the package-boundary cast.

transformTracingResponse(...) is asserted to be TraceSpanNode[] through unknown, so any drift between @agenta/entities and the local trace shape will reach cursor extraction and rendering unchecked. Prefer a compatible return type or explicit mapper, and add a contract test for both traces and spans responses.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 726d6c9a-1864-47cc-a8a1-9964f4542c22

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2b70a and 56fcef8.

📒 Files selected for processing (219)
  • docs/design/simplify-nav-new-users/README.md
  • docs/design/simplify-nav-new-users/context.md
  • docs/design/simplify-nav-new-users/plan.md
  • docs/design/simplify-nav-new-users/research.md
  • docs/design/simplify-nav-new-users/status.md
  • web/ee/next.config.ts
  • web/ee/src/components/PostSignupForm/PostSignupHeader.tsx
  • web/ee/src/components/pages/app-management/components/ApiKeyInput.tsx
  • web/ee/src/components/pages/overview/deployments/HistoryConfig.tsx
  • web/ee/src/components/pages/settings/Billing/Modals/PricingModal/assets/SubscriptionPlanDetails/index.tsx
  • web/ee/src/components/pages/settings/Billing/Modals/PricingModal/assets/types.d.ts
  • web/ee/src/components/pages/settings/Billing/index.tsx
  • web/ee/tsconfig.json
  • web/oss/next.config.ts
  • web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx
  • web/oss/src/components/DeploymentsDashboard/Table/assets/deploymentColumns.tsx
  • web/oss/src/components/DeploymentsDashboard/assets/UseApiContent.tsx
  • web/oss/src/components/DeploymentsDashboard/modals/DeploymentConfirmationModalWrapper.tsx
  • web/oss/src/components/DrillInView/TraceSpanDrillInView.tsx
  • web/oss/src/components/DrillInView/viewModes.ts
  • web/oss/src/components/EditorViews/SimpleSharedEditor/index.tsx
  • web/oss/src/components/EditorViews/SimpleSharedEditor/types.ts
  • web/oss/src/components/EditorViews/assets/helper.ts
  • web/oss/src/components/EnhancedUIs/Drawer/index.tsx
  • web/oss/src/components/EntityIdentity/useRenameApp.ts
  • web/oss/src/components/EvalRunDetails/Table.tsx
  • web/oss/src/components/EvalRunDetails/atoms/metricProcessor.ts
  • web/oss/src/components/EvalRunDetails/atoms/metrics.ts
  • web/oss/src/components/EvalRunDetails/atoms/query.ts
  • web/oss/src/components/EvalRunDetails/atoms/runMetrics.ts
  • web/oss/src/components/EvalRunDetails/atoms/runMetrics/types.ts
  • web/oss/src/components/EvalRunDetails/atoms/scenarioColumnValues.ts
  • web/oss/src/components/EvalRunDetails/atoms/scenarioSteps.ts
  • web/oss/src/components/EvalRunDetails/atoms/table/columnAccess.ts
  • web/oss/src/components/EvalRunDetails/atoms/table/columns.ts
  • web/oss/src/components/EvalRunDetails/atoms/table/run.ts
  • web/oss/src/components/EvalRunDetails/atoms/table/scenarios.ts
  • web/oss/src/components/EvalRunDetails/atoms/table/testcases.ts
  • web/oss/src/components/EvalRunDetails/atoms/table/types.ts
  • web/oss/src/components/EvalRunDetails/atoms/tableRows.ts
  • web/oss/src/components/EvalRunDetails/atoms/traces.ts
  • web/oss/src/components/EvalRunDetails/atoms/types.ts
  • web/oss/src/components/EvalRunDetails/atoms/variantConfig.ts
  • web/oss/src/components/EvalRunDetails/components/CompareRunsMenu.tsx
  • web/oss/src/components/EvalRunDetails/components/EvalTestcaseDrawerAdapter/drawerPayload.ts
  • web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsChart/BarChart.tsx
  • web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsChart/index.tsx
  • web/oss/src/components/EvalRunDetails/components/FocusDrawer.tsx
  • web/oss/src/components/EvalRunDetails/components/TableCells/MetricCell.tsx
  • web/oss/src/components/EvalRunDetails/components/columnVisibility/ColumnVisibilityPopoverContent.tsx
  • web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/ContextChipList.tsx
  • web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/EvaluatorSection.tsx
  • web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/InvocationSection.tsx
  • web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/utils.ts
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/BaseRunMetricsSection.tsx
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/EvaluatorTemporalMetricsChart.tsx
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/MetadataSummaryTable.tsx
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/MetricComparisonCard.tsx
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/OverviewPlaceholders.tsx
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/hooks/useRunMetricData.ts
  • web/oss/src/components/EvalRunDetails/components/views/OverviewView/utils/evaluatorMetrics.ts
  • web/oss/src/components/EvalRunDetails/components/views/SingleScenarioViewerPOC/ScenarioAnnotationPanel/useAnnotationState.ts
  • web/oss/src/components/EvalRunDetails/components/views/SingleScenarioViewerPOC/types.ts
  • web/oss/src/components/EvalRunDetails/hooks/usePreviewTableData.ts
  • web/oss/src/components/EvalRunDetails/utils/buildPreviewColumns.tsx
  • web/oss/src/components/EvalRunDetails/utils/buildSkeletonColumns.ts
  • web/oss/src/components/EvalRunDetails/utils/renderChatMessages.tsx
  • web/oss/src/components/EvaluationRunsTablePOC/atoms/view.ts
  • web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsDeleteButton.tsx
  • web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/export/referenceResolvers.ts
  • web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx
  • web/oss/src/components/EvaluationRunsTablePOC/components/cells/RunMetricCell/index.tsx
  • web/oss/src/components/EvaluationRunsTablePOC/components/filters/EvaluationRunsHeaderFilters.tsx
  • web/oss/src/components/EvaluationRunsTablePOC/types.ts
  • web/oss/src/components/Evaluations/MetricDetailsPopover/assets/ResponsiveMetricChart.tsx
  • web/oss/src/components/Evaluations/components/MetricDetailsPreviewPopover.tsx
  • web/oss/src/components/Evaluators/Drawers/HumanEvaluatorDrawer/index.tsx
  • web/oss/src/components/Evaluators/assets/evaluatorFiltering.ts
  • web/oss/src/components/Filters/Filters.tsx
  • web/oss/src/components/InfiniteVirtualTable/atoms/columnHiddenKeys.ts
  • web/oss/src/components/InfiniteVirtualTable/columns/cells.tsx
  • web/oss/src/components/InfiniteVirtualTable/columns/createStandardColumns.tsx
  • web/oss/src/components/InfiniteVirtualTable/columns/types.ts
  • web/oss/src/components/InfiniteVirtualTable/components/InfiniteVirtualTableInner.tsx
  • web/oss/src/components/InfiniteVirtualTable/hooks/useColumnVisibility.ts
  • web/oss/src/components/InfiniteVirtualTable/hooks/useExpandableRows.tsx
  • web/oss/src/components/InfiniteVirtualTable/hooks/useTableKeyboardShortcuts.ts
  • web/oss/src/components/InfiniteVirtualTable/types.ts
  • web/oss/src/components/Layout/assets/Breadcrumbs.tsx
  • web/oss/src/components/Onboarding/tours/evaluationResultsTour.ts
  • web/oss/src/components/Placeholders/EmptyComponent/index.tsx
  • web/oss/src/components/Playground/Components/Menus/SelectVariant/index.tsx
  • web/oss/src/components/Playground/Components/Modals/CreateVariantModal/assets/types.d.ts
  • web/oss/src/components/Playground/Components/Modals/CreateVariantModal/index.tsx
  • web/oss/src/components/Playground/Components/Modals/DeployVariantModal/types.d.ts
  • web/oss/src/components/Playground/Components/PlaygroundFocusDrawerAdapter/drawerPayload.ts
  • web/oss/src/components/Playground/Components/TestsetDropdown/TestsetPreviewPanelWrapper.tsx
  • web/oss/src/components/Playground/Components/TestsetDropdown/index.tsx
  • web/oss/src/components/Playground/Components/WebWorkerProvider/index.tsx
  • web/oss/src/components/Playground/Playground.tsx
  • web/oss/src/components/PlaygroundRouter/PlaygroundLoadingShell.tsx
  • web/oss/src/components/References/cells/ApplicationCells.tsx
  • web/oss/src/components/References/cells/TestsetCells.tsx
  • web/oss/src/components/RequireWorkflowKind/index.tsx
  • web/oss/src/components/SessionInspector/tabs/StreamsTab.tsx
  • web/oss/src/components/SharedDrawers/AddToTestsetDrawer/atoms/drawerState.ts
  • web/oss/src/components/SharedDrawers/AddToTestsetDrawer/components/DataPreviewEditor.tsx
  • web/oss/src/components/SharedDrawers/AddToTestsetDrawer/hooks/useTestsetDrawer.ts
  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/AnnotateDrawerTitle/index.tsx
  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/SelectEvaluators/index.tsx
  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/hooks/useEvaluatorSchemas.ts
  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/transforms.ts
  • web/oss/src/components/SharedDrawers/AnnotateDrawer/assets/types.d.ts
  • web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionDrawerButton/index.tsx
  • web/oss/src/components/SharedDrawers/SessionDrawer/store/sessionDrawerStore.ts
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/AccordionTreePanel.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/AnnotationTabItem/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/OverviewTabItem/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceContent/components/TraceTypeHeader/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceHeader/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceSidePanel/TraceLinkedSpans/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceSidePanel/TraceReferences/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/index.tsx
  • web/oss/src/components/SharedDrawers/TraceDrawer/store/openInPlayground.ts
  • web/oss/src/components/SharedDrawers/TraceDrawer/store/traceDrawerStore.ts
  • web/oss/src/components/Sidebar/components/ListOfOrgs.tsx
  • web/oss/src/components/Sidebar/components/ListOfProjects.tsx
  • web/oss/src/components/Sidebar/dynamic/registry.ts
  • web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx
  • web/oss/src/components/TestcasesTableNew/components/CommitTestsetModal.tsx
  • web/oss/src/components/TestcasesTableNew/components/TestcaseRowActionsDropdown.tsx
  • web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx
  • web/oss/src/components/TestcasesTableNew/hooks/useTestcasesTable.ts
  • web/oss/src/components/TestsetsTable/components/TestsetsHeaderFilters.tsx
  • web/oss/src/components/TestsetsTable/hooks/useTestsetsColumns.tsx
  • web/oss/src/components/VariantsComponents/store/registryStore.ts
  • web/oss/src/components/Webhooks/WebhookLogsTab.tsx
  • web/oss/src/components/Webhooks/utils/buildPreviewRequest.ts
  • web/oss/src/components/Webhooks/utils/buildSubscription.ts
  • web/oss/src/components/pages/agent-home/PlaygroundOnboarding/OnboardingLoader.tsx
  • web/oss/src/components/pages/agents/AgentsPage.tsx
  • web/oss/src/components/pages/app-management/modals/CreateAppStatusModal.tsx
  • web/oss/src/components/pages/app-management/modals/CustomWorkflowModal/hooks/types.d.ts
  • web/oss/src/components/pages/evaluations/NewEvaluation/Components/NewEvaluationModalContent.tsx
  • web/oss/src/components/pages/evaluations/NewEvaluation/Components/NewEvaluationModalInner.tsx
  • web/oss/src/components/pages/evaluations/NewEvaluation/Components/SelectEvaluatorSection/SelectEvaluatorSection.tsx
  • web/oss/src/components/pages/evaluations/NewEvaluation/types.ts
  • web/oss/src/components/pages/evaluations/cellRenderers/cellRenderers.tsx
  • web/oss/src/components/pages/evaluations/onlineEvaluation/assets/helpers.ts
  • web/oss/src/components/pages/evaluations/onlineEvaluation/components/FiltersPreview.tsx
  • web/oss/src/components/pages/evaluations/utils.ts
  • web/oss/src/components/pages/observability/assets/getObservabilityColumns.tsx
  • web/oss/src/components/pages/observability/components/NodeNameCell.tsx
  • web/oss/src/components/pages/observability/components/ObservabilityHeader/index.tsx
  • web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx
  • web/oss/src/components/pages/observability/components/SessionsTable/assets/getSessionColumns.tsx
  • web/oss/src/components/pages/observability/components/SessionsTable/index.tsx
  • web/oss/src/components/pages/observability/components/TimestampCell.tsx
  • web/oss/src/components/pages/observability/dashboard/CustomAreaChart.tsx
  • web/oss/src/components/pages/overview/variants/VariantPopover.tsx
  • web/oss/src/components/pages/prompts/PromptsPage.tsx
  • web/oss/src/components/pages/settings/Organization/index.tsx
  • web/oss/src/hooks/usePostAuthRedirect.ts
  • web/oss/src/lib/Types.ts
  • web/oss/src/lib/api/assets/fetchClient.ts
  • web/oss/src/lib/evaluations/legacy.ts
  • web/oss/src/lib/helpers/analytics/AgPosthogProvider.tsx
  • web/oss/src/lib/helpers/dateTimeHelper/index.ts
  • web/oss/src/lib/hooks/useEvaluationRunMetrics/index.ts
  • web/oss/src/lib/hooks/useEvaluationRunMetrics/types.ts
  • web/oss/src/lib/hooks/usePreviewEvaluations/index.ts
  • web/oss/src/lib/hooks/usePreviewEvaluations/types.ts
  • web/oss/src/lib/onboarding/atoms.ts
  • web/oss/src/lib/onboarding/index.ts
  • web/oss/src/lib/onboarding/widget/store.ts
  • web/oss/src/lib/traces/traceUtils.ts
  • web/oss/src/pages/auth/[[...path]].tsx
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/index.tsx
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/overview/index.tsx
  • web/oss/src/pages/workspaces/accept.tsx
  • web/oss/src/services/evaluationRuns/api/index.ts
  • web/oss/src/services/evaluationRuns/api/types.ts
  • web/oss/src/services/evaluations/api/index.ts
  • web/oss/src/services/observability/types/index.ts
  • web/oss/src/services/organization/api/index.ts
  • web/oss/src/services/runMetrics/api/index.ts
  • web/oss/src/services/tracing/types/index.ts
  • web/oss/src/services/workspace/index.ts
  • web/oss/src/state/app/atoms/fetcher.ts
  • web/oss/src/state/app/selectors/app.ts
  • web/oss/src/state/appCreation/status.ts
  • web/oss/src/state/appState/hooks.ts
  • web/oss/src/state/entities/shared/createPaginatedEntityStore.ts
  • web/oss/src/state/newObservability/atoms/controls.ts
  • web/oss/src/state/newObservability/atoms/queries.ts
  • web/oss/src/state/newObservability/atoms/queryHelpers.ts
  • web/oss/src/state/newObservability/helpers/index.ts
  • web/oss/src/state/newObservability/selectors/tracing.ts
  • web/oss/src/state/newPlayground/workflowEntityBridge.ts
  • web/oss/src/state/onboarding/selectors.ts
  • web/oss/src/state/org/index.ts
  • web/oss/src/state/project/index.ts
  • web/oss/src/state/project/selectors/project.ts
  • web/oss/src/state/url/auth.ts
  • web/oss/src/state/url/focusDrawer.ts
  • web/oss/src/state/url/index.ts
  • web/oss/src/state/url/playground.ts
  • web/oss/src/state/workspace/atoms/mutations.ts
  • web/oss/tsconfig.json
  • web/packages/agenta-entities/src/shared/paginated/createPaginatedEntityStore.ts
  • web/packages/agenta-entities/src/workflow/index.ts
  • web/packages/agenta-entity-ui/src/variant/components/EnvironmentStatus.tsx
  • web/packages/agenta-ui/src/Editor/index.ts
  • web/packages/agenta-ui/src/InfiniteVirtualTable/columns/createStandardColumns.tsx
  • web/packages/agenta-ui/src/InfiniteVirtualTable/features/InfiniteVirtualTableFeatureShell.tsx
  • web/packages/agenta-ui/src/InfiniteVirtualTable/types.ts
  • web/packages/agenta-ui/src/drill-in/context/DrillInUIContext.tsx
  • web/tsc-error-inventory.md

Comment thread web/oss/src/components/EvalRunDetails/atoms/metrics.ts
Comment thread web/oss/src/components/EvalRunDetails/atoms/runMetrics.ts
@ashrafchowdury
ashrafchowdury changed the base branch from release/v0.106.0 to main July 26, 2026 10:10
@ashrafchowdury
ashrafchowdury changed the base branch from main to release/v0.106.0 July 26, 2026 10:11
2 phase:
1. is hide the page
2. add a switch toggle on settings profile tab to get back the llm apps view - this is for people who is older user and invitated there team member to work on
… durable per-user nav default to control the simplified,

-focused sidebar for new signups without impacting existing users.

- export navSimplDefaultAtom from onboarding index- read and set navSimplifiedDefaultAtom in post-auth redirect hook so signup flow seeds the simplified-nav default when creating new users- add advancedNavHiddenAtom selector that derives from the new durable navSimplifiedAtom (separating signup-era default from transient isOnboarding/isUser state)
- update design docs to explain the new atom, the rationale (do not reuse sticky isNewUserAtom) and the1/Phase2 migration seamThis prevents existing users from losing advanced navigation due to thesticky isNewUser flag while allowing newups to get a simplifiedsidebar by default.
@ashrafchowdury
ashrafchowdury force-pushed the feat/simplify-platform-for-first-signup-users branch from 56fcef8 to 96f8a20 Compare July 26, 2026 13:58
@ashrafchowdury

ashrafchowdury commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

My bad, I have updated the branch @mmabrouk

@ashrafchowdury

Copy link
Copy Markdown
Contributor Author

This #5273 became relevant again. So after merging this PR, we should merge this PR

mmabrouk added 3 commits July 27, 2026 22:41
…ents

Starting a new agent puts an existing user into an onboarding session, and the
sidebar greyed out Agents, Evaluation runs and Annotation Queues for the whole
session. Those users watched their own agents sit listed under a disabled
parent, with no way to click through.

deadEndNavDisabledAtom asked "is onboarding running" when it meant "would this
page be empty". It now also requires the project to have no agents. The agent
list is read inside the guard, never above it, so the query stays untracked and
unfetched whenever onboarding is off, matching what gatedSidebarSource does for
the lazy submenu.

Claude-Session: https://claude.ai/code/session_01Tjg6u7XP8huKzZqq4kGBmA
…ags tab

Settings tabs now declare which scope they belong to (project, organization or
personal) and the sidebar renders them under those headings, with a new
Organization General tab for renaming, transferring and deleting an org.

Adds a Feature Flags tab holding two per-user switches, both stored per user in
local storage: one turns the playground session inspector on, the other
overrides the simplified navigation chosen at signup. The nav override sits in
front of the durable signup default (override ?? default), so an existing user
who wants the simplified nav, or a new user who wants the full one, can say so
without touching the signup-era value. The session inspector is now off unless
its flag is on, in both the chat slice and the panel button.

Also updates the simplify-nav design workspace to match.

Claude-Session: https://claude.ai/code/session_01Tjg6u7XP8huKzZqq4kGBmA
… rejection

antd passes onOk straight to the OK button's onClick, so the awaited
mutateAsync in handleDelete rejected into nothing when the backend refused
the delete. The mutation's onError already shows the message, so catch and
return instead of letting the rejection escape.

Claude-Session: https://claude.ai/code/session_01Tjg6u7XP8huKzZqq4kGBmA
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jul 27, 2026
@mmabrouk

Copy link
Copy Markdown
Member

Review of the three commits pushed just now

What landed

  • 946524b7 fixes the sidebar greying out Agents, Evaluation runs and Annotation Queues for a user who already has agents. deadEndNavDisabledAtom asked "is onboarding running" when it meant "would this page be empty", so starting a second agent disabled the nav for the whole session while the user's own agents sat listed under the disabled parent. It now also requires the project to have no agents. The agent list is read inside the guard, never above it, so the query stays untracked and unfetched whenever onboarding is off, matching what gatedSidebarSource does for the lazy submenu. Verified live: reproduced the onboarding state on a project with four agents and confirmed the entry renders enabled.
  • 22b03720 groups settings tabs by scope and adds the personal Feature flags tab.
  • ce54191e is a review fix, described below.

Fixed during review

A failed organization delete escaped as an unhandled promise rejection. antd passes onOk straight to the OK button's onClick, so the awaited mutateAsync in handleDelete rejected into nothing when the backend refused the delete, for example on the last-organization guard. The mutation's onError already shows the message, so the call now catches and returns.

Open questions for the reviewer

  1. Projects tab has no permission gate. It moved from URL-only to a visible sidebar entry under Organization, which the new test asserts, so the move looks intended. But isSettingsTabVisible has no projects case, so it falls through to default: return true and every workspace member now sees a discoverable page that creates and deletes projects. Worth deciding whether it should be access.isOwner.
  2. The playground inspector is now off for everyone, not only new signups. playgroundInspectorEnabledAtom defaults to false, and AgentConversation, InspectSessionButton and PanelSessionInspectorButton are all gated on it. Existing users lose the session and turn inspector with no hint that it moved to Settings, Feature flags, Debug flags. If the goal is to simplify only for first-signup users, defaulting it from the same signal navSimplifiedDefaultAtom uses would preserve it for everyone else.

Smaller findings, not fixed

  • showInSidebar is now dead. No SETTINGS_TABS entry sets it, so the filter in getSettingsSidebarTabs is a permanent no-op while the optional field still suggests a gate that no longer exists.
  • Deleting your last non-demo organization leaves you on the deleted org's URL. remainingOrgs comes from orgsAtom, which filters demo orgs, while the backend's last-organization guard counts them, so a user whose only other org is a demo org passes the backend check and gets no redirect.
  • Both Feature flags switches no-op silently if clicked before onboardingStorageUserIdAtom resolves, because the writers return early on a null user id. The window is short but the toggle animates and snaps back with no error.
  • web/oss defines no test: script, so run-tests.ts --layer unit skips it and none of the roughly ten *.test.ts files under web/oss/src, including the 53 new lines in navigation.test.ts, run in CI or locally. Adding a test:unit script to web/oss/package.json would make them count.
  • The comment above the default_workspace cast in General.tsx describes a latent bug from the list endpoint, but selectedOrg comes from the details query, so the field is always present.

Checks run: tsc clean on web/oss, eslint clean on every changed file. The settings navigation tests were not run, for the reason in the fourth bullet above.

@mmabrouk
mmabrouk changed the base branch from release/v0.106.0 to temp-106.1 July 28, 2026 12:49
@mmabrouk
mmabrouk merged commit 56504b0 into temp-106.1 Jul 28, 2026
60 checks passed
ashrafchowdury added a commit that referenced this pull request Sep 9, 2026
* refactor(frontend): move the Classic mode preference into @agenta/shared

The switch has only ever driven the desktop sidebar, so its atoms sat in
web/oss/src/lib/onboarding. It is about to decide which APP a user gets, and
the app that needs to read it — and to offer the switch that leaves it — is
/m, which is lint-banned from importing @/oss/*.

Move the pair to @agenta/shared/state, under the SAME agenta:onboarding: keys:
a changed prefix would silently reset every existing user to their signup-era
default. Add classicModeEnabledAtom so both Preferences pages bind one thing
rather than each inverting advancedNavHidden by hand.

The atoms gain getOnInit so they read storage on first get instead of on mount.
Without it the value starts at the default and corrects a tick later, and the
gate would act on the wrong answer first.

SESSION_QUERY_PARAM moves down to @agenta/shared too, re-exported from
@agenta/sessions/link: the route map that translates one app's URL into the
other's needs the same string, and a second copy is a silent deep-link break.

* feat(frontend): send users without Classic mode to /m

Classic mode off has meant a simplified desktop sidebar. It now means the
simplified APP: those users land in /m for every page /m has, and the desktop
keeps the rest.

Nobody is opted in by this commit who was not already opted in by signup —
setNavSimplifiedDefault(true) has fired for every new user since 2026-07-28
(#5478), and existing users default to classic on and stay where they are.

Built on the device gate rather than beside it, so there is one route map and
one exception list. mapDesktopToMobile stops sending every project route to the
sessions list and maps per route; the new mobileRouteFor returns null for pages
/m does not have (evaluations, test sets, prompts, evaluators, annotations),
which the preference gate reads as "leave them on desktop" and the device gate
still reads as "/m root" — a phone cannot use those pages either way.

The desktop playground maps to a session's chat, since /m has no playground
route: the session page IS the playground there, and ?agent= names the agent a
session with no turns cannot name for itself.

Two layers, because the preference is localStorage and the middleware can only
read cookies: the apps mirror it to agenta-classic-mode, and the first visit —
which has no cookie yet — is caught in the client instead. An absent cookie is
UNKNOWN, never "off": the preference is per-browser, so guessing would move
users who never chose the simplified experience.

The client hop waits for a settled session and never fires from /auth. Post-auth
sets the user id and the signup default and THEN routes onward, so a user is
briefly known while still on the sign-in page; jumping apps there races the
router and skips a new EE user's post-signup survey outright.

Leaving is the switch in /m's Settings > Preferences, which carries the user to
the desktop equivalent of the page they were on. There is deliberately no
reverse gate — a Classic-mode user who opens /m stays there.

AGENTA_CLASSIC_MODE_GATE (default on) disables it per deployment, independent of
AGENTA_MOBILE_GATE so device detection is not caught in the same switch.

Also drops web/mobile's declared copy of the gate for the real import, as its
own header asked once /m had workspace deps — the alternative was writing all
of the above twice.

* fix(frontend): drop getOnInit from the Classic mode atoms

It read localStorage during the first render, which diverges from prerendered
HTML and breaks hydration wherever the value renders. The desktop was safe --
its Layout is dynamic({ssr: false}) -- but /m's pages are statically
prerendered, and its Preferences tab renders this into a Switch. The
neighbouring flags (voice input, playground inspector) omit it for the same
reason.

The gate never needed it. Both the cookie sync and the redirect are effects on
this value, so they re-run when it settles a tick later: late, never wrong. The
worst case is a cookie that briefly says "classic on", which the next render
corrects.

Tests mount the atoms instead, which is how the app reads them anyway --
atomWithStorage reads storage in onMount, so an unsubscribed get reports the
default rather than what is stored.

* feat(frontend): derive the simplified default from the account, not the browser

The signup-era default was written to localStorage on whichever browser
completed signup, and nothing ever re-seeds it: is_new_user comes from
_create_account, which is idempotent and returns False once a user row exists.
So the same person read "simplified" on their signup browser and "classic"
everywhere else -- and it never healed.

That was survivable while the flag only hid four nav items. It is not now that
it decides which app you get.

GET /profile already returns created_at; the zod schema at the boundary was
dropping it. Let it through, and read the default as "stored flag OR account
created after the cutoff". The signup browser keeps answering exactly as it
always has, and a second device stops disagreeing with it.

The cutoff is padded past the release that started seeding the flag (v0.106.1,
on main 2026-07-30) rather than the merge date of the change itself (#5478,
2026-07-28), because the seed is written client-side -- what matters is when the
frontend carrying it reached users. Padding forward only ever leaves someone on
the status quo, since the stored flag still covers anyone it misses.

The explicit override stays browser-local. Fixing that needs somewhere to put
it, and there is no user-settings column or endpoint -- the users table is id,
uid, username, email and timestamps. It affects only people who deliberately
flipped the switch, rather than the whole cohort.

* style(frontend): apply prettier to the gate tests

* fix(frontend): stop the classic gate and the device gate bouncing each other

Two gates, both able to redirect, neither aware of the other. With
AGENTA_MOBILE_GATE on and a desktop browser whose Classic mode is off, the
desktop gate sent the request to /m for the preference, the mobile gate sent it
straight back for the device, and the cookie that started it never changed. An
infinite ping-pong, on exactly the flag combination the previous commit
advertised as independent.

Classic mode off means /m is where that user belongs, so the device heuristic
must not overrule it. decideMobileGate now passes on the classic cookie, gated
on the same flag, and web/mobile's proxy reads that flag so both halves agree.

The regression test asserts both directions of one request: the desktop gate
still wants them in /m, and the mobile gate leaves them there. Asserting only
the second would hide the loop rather than break it.

Also makes AGENTA_CLASSIC_MODE_GATE a real kill switch. It is a bare variable
resolved server-side, so the client redirect could not see it: turning the flag
off stopped the middleware and left the browser redirecting anyway, which is
worse than no switch at all. entrypoint.sh mirrors it into __env.js, the same
mechanism every other runtime flag uses, and the hook reads it from there.

* fix(frontend): normalize the backend timestamp before Date.parse

`str(datetime)` produces "2026-08-01 12:34:56.789012+00:00". Two things there
are outside the format Date.parse is required to accept: the space separator,
which Safari refuses, and six fractional digits where the spec defines exactly
three. Past the third digit the behaviour is implementation-defined, so an
engine is free to answer NaN.

Every major engine copes today, but the failure mode here is silent. NaN reads
as "not in the simplified cohort", which parks a post-cutoff account on classic
mode and quietly withholds the redirect, with nothing to show for it.

Truncate to milliseconds alongside the separator fix, and cover the shapes the
backend can actually emit. The added cutoff-boundary test is there because
truncation must not shift the instant across it.

* chore(frontend): trim the new comments to the house rule

* fix(frontend): drop the Input import the menu restructure left unused

* fix(frontend): drop the jotai hooks the merge left unused

* fix(frontend): expand a thought while the model is still reasoning on /m

The fold read liveness from isLiveTextItem, which only ever matches a text part, so a
reasoning part was always false: the thought stayed collapsed on "Thought" mid-stream and a
reasoning-only turn looked frozen. Read the reasoning part own AI SDK state instead, as the
desktop does. Closes #6547.

* feat(frontend): give the mobile transcript a real loading state

ChatLoading was a LITE-phase one-liner, so opening a busy playground showed small "Loading..."
text against an empty screen for seconds. Replace it with a skeleton that mirrors TurnRow
geometry, matching what the other mobile features already do. Closes #6543.

* refactor(frontend): share the agent inline-rename editor through @agenta/entity-ui

The editor was oss-only because it called useRenameApp directly. Inject the commit instead, so
the desktop keeps its app-management cache refresh and mobile can reuse the same component.
Extract useRenameAgent from useAgentActions so the modal and the inline editor share one write.

* fix(frontend): render mobile code blocks through the desktop markdown renderer

Streamdown emits one span per code line with no newline text nodes and only classes it when
lineNumbers is on, so a fenced block collapsed onto one row on /m. The desktop already carried
the fix; /m had its own Streamdown wrapper and never got it.

Lift the desktop renderer into @agenta/chat as ChatMarkdown, with the token layer and the drive
link resolver injected so no antd names ship in the package, and put both surfaces on it. Mobile
also picks up the Shiki and KaTeX plugins it was missing. Closes #6550.

* fix(frontend): settle a questionnaire when Skip has nowhere left to go

For a single-question form lastIndex is 0, so skip() advanced to the step it was already on and
nothing settled: the run parked and Skip looked dead. It was broken on both surfaces, not just
/m. Route Skip through the decline path the review screen already uses when there is no next
step and no review to land on; multi-step skip-to-review is unchanged.

Also give the question room to be read on a phone: the dock label becomes a height-capped scroll
region at the exact two-line height desktop had, so the no-shift contract holds while the whole
prompt stays reachable. Closes #6551, closes #6552.

* fix(frontend): stop the browser tab title flapping through "Agenta"

Both surfaces rendered their title from a query that is undefined on the first frames, and
formatPageTitle(undefined) returns the bare product name — so every deep link and every session
switch flashed "Agenta" before the real name. Three routes also rendered a bare PageTitle as a
deliberate placeholder, and mobile _app set no default at all.

Emit no title element while the name is still undefined, so the standing title holds; give
mobile an app-wide default; drop the placeholders; and put both apps on one shared PageTitle in
@agenta/ui so they cannot drift again. Closes #6545.

* fix(frontend): give the mobile panes the desktop resize range

/m capped the config pane at 440 and the files pane at 560, far under /w (900 and 1600), and
held both widths in useState so a drag never survived a reload. Since /m now serves desktop too,
move the constants and both width atoms into @agenta/chat/state and have both surfaces read
them. SplitPane still clamps to the available width, so a phone never reaches the larger caps.
Closes #6540.

* fix(frontend): hold the agent id across a session tab switch

ChatScreen already held entityId in a ref so it could not blink to null mid-switch, but passed
agentId raw. While the new session re-resolved, the files-pane scope fell back to the session id
and read a fresh false, so the open pane animated shut and back; the tab-order scope collapsed
to __project__ at the same moment, dropping the saved arrangement and the list filter with it.
Hold agentId the same way. Closes #6542.

Then stop the order drifting on its own: seed the saved tab order from the first observed rows
instead of no-oping until a hand-drag, append unseen ids at the tail rather than the head, and
give the desktop history sort a stable tiebreak so refetched activity stops swapping equal rows.
Closes #6544.

* feat(frontend): let the mobile playground rename an agent and change its icon

AgentPageHeader already documents both slots as host-supplied, but /m passed a plain string name
and a read-only chip, so neither was reachable outside the agent overview. Fill the slots with
the shared inline name editor and a chip that opens the AgentIconSheet /m already ships.

The pen is always visible here rather than hover-revealed: touch has neither hover nor
double-click.

* refactor(frontend): share the pane coexistence threshold through @agenta/chat

The rule that keeps the config pane, the transcript and the Files pane from squeezing each other
lived in web/oss, so /m — which now serves desktop too — had none and let both side panes stay
open at any width from md up.

Move the derivation and its media-query hook into @agenta/chat/state, taking the sidebar width as
an argument so the package needs no dependency on @agenta/navigation. Desktop behaviour is
unchanged; rightPanel.ts existed only for this one constant, so it goes.

* feat(frontend): give /m an open-tab set and Chrome-style bulk close

/m derived its tab strip straight from the server session list, so there was nothing to close and
no close affordance at all. Add a local, persisted open-tab set keyed by project and agent scope,
with the desktop close semantics: nearest-survivor activation, pinned tabs excluded from both
bulk closes, and each entry disabled when it would do nothing.

Closing never touches the server — a closed session stays in history and rejoins the set when you
navigate back. An unseeded scope renders every listed session and that first list is the seed, so
nobody meets an empty rail. Ids that fall out of the capped server window are kept, not closed.

Closing the active tab routes to the survivor BEFORE dropping it: the other way round, the
always-open rule for the active session reopened the tab a beat later. Closes #6538.

* fix(frontend): get antd out of shared package code

Two agenta-entity-ui files imported antd directly. /m declares no antd and mounts no
ConfigProvider, so the files-pane download tooltip and the trigger-deliveries skeleton rendered
unstyled there — --ant-color-* is undefined on that host. Swap both for the shared kit.

agenta-entity-ui is now antd-free. agenta-playground-ui still has 32 such imports and /m depends
on it, so the same hazard is live there; it needs its own pass.

* fix(frontend): re-sync the mobile token bridge with the palette

/m read as a downgraded skin of /w because the bridge had drifted, not because components were
missing. Four fixes:

- --border mapped to border.secondary, one step fainter than /w everywhere, since globals.css
  applies border-border to every element. Point it at border.default, as --input already was.
- The control scale was hand-copied and wrong: buttons and fields were a full type step small,
  with two wrong line-heights. Re-sync it and extend tokens:check to guard it, so it cannot
  drift again silently.
- Popover, select and sheet used plain drop shadows, which are invisible on a #141414 page.
  Use shadow-overlay, which carries the separating ring — it was declared on /m with no usages.
- Drawer shadows were light-only literals, so a sheet in dark had no ring either. Bridge the
  dark side only: the palette light values are antd() refs the desktop keeps so it tracks antd
  upgrades, and resolving them would freeze antd output into the source of truth. Mobile has no
  antd to track, so it keeps local light literals behind a var() fallback.

sheet.tsx now takes the drawer tokens instead of inline literals, so the two paths cannot drift.

* fix(frontend): step the mobile prose scale up at desktop width

Markdown was hard-clamped to 12px with no breakpoint, so the transcript read phone-sized on a
desktop window. Step body text up at sm to the 14px /w uses; headings, tables and code blocks
already matched.

* fix(frontend): put /m on the desktop agent icon picker

/m had a bespoke lookalike, so every state differed: 32px swatches wrapped onto two rows against
the desktop 20px single row, a 6-column unvirtualised grid against 8 virtualised, no "Saves as
you pick" subtitle, a Done footer with no desktop counterpart, and — worst — custom colour opened
the native OS colour dialog instead of the in-panel hex field, saturation square and hue strip.

Render the shared AgentIconPicker instead, wired exactly as the desktop wires it, and delete the
seven mobile files it replaces. The overview screen moves from a sheet to the same popover, so
both entry points are one container and cannot drift.

Two additive touch fixes in the shared picker: listen for pointercancel, since a touch drag the
browser reclaims never fires pointerup and left listeners leaked with the colour uncommitted, and
touch-none on the square and strip so a finger paints instead of scrolling. Mouse input is
unaffected.

* style(frontend): move the shortcuts button to the top header right corner

It sat on the session tab strip; the desktop puts it at the playground header right edge, which
is where it reads as belonging to the surface rather than to the tabs.

* fix(frontend): drop the instructions drawer aside on narrow screens

The drawer is min(920, viewport), so below md its fixed 240px side column left the markdown
editor a few characters wide. Hide the column there — the tips and the suggested scaffolds are
aids, not the task.

* fix(frontend): give /m a real DrillIn bridge instead of a stub

/m supplied only workflowReference, and that one gap broke four things: the Integrations section
did not render at all for an agent with zero integrations (the control nulls its drawer opener
without gatewayTools, then hides the section at count 0), the model picker lost its vault
provider groups and the Add provider footer, the subagent Open agent link was dead, and the
MCP/Skill/Tool controls had no editor context.

Rather than reproduce the desktop provider, promote its host-neutral half into the packages so
both hosts mount the same pieces: GatewayToolsBridgeProvider into @agenta/entity-ui/drill-in
(keeping the two-component split so the connections query still never runs when tools are off),
and useLLMProviderConfig with its ConfigureProviderDrawer into @agenta/entity-ui/secretProvider.
Only the genuinely host-specific values stay local — the agent href, built from /m routes, and
the cloud flag.

* feat(frontend): let /m expand a tool call to its input and output

/m rendered one static line per tool, so with the trace drawer also blank there was no path at
all to see what a tool did. Give each row the desktop caret and collapse, holding the tool name,
its input, and its output or error — every payload capped and separately scrollable, with
overscroll contained so a touch scroll does not drag the transcript.

Most of it was already shared: the sentence, the summary, the display resolver, and the expanded
state, which is keyed per tool call so a row stays open across its turn unmounting. Only the
labelled payload block was inline in oss, so it moves to @agenta/chat as ToolIOBlock.

Also correct a real bug on the way: an approval-responded call was painted with a green check and
no verdict, claiming success for a gate that may have been denied. And wire Deny all, which the
shared ApprovalCard already supported and the mobile dock never passed.

* fix(frontend): put every config row on one trailing axis

The config pane stacks three kinds of row built by different components, and
their right edges did not line up: region header bars (Configuration, Triggers,
Files), accordion section headers, and the Files browse control.

Two causes. The header bars carried w-full with px-4, and preflight is off in
this repo, so those content-box divs measured 32px wider than the sections below
and pushed their trailing content past the panel edge. Stretching in the
enclosing flex column gives the same width and the same content box.

The other is the caret. Both phosphor carets draw their ink inset from their own
box by different amounts, so a bare glyph sat off the axis and MOVED when a row
toggled open. ConfigRowTrailing is the shared convention: a fixed 14px
affordance column every row reserves whether or not it has a glyph, with the
caret transform cancelling the ink inset in one place instead of at call sites.

* feat(frontend): make the session and agent menus work on /m

Rename rendered as a menu item on five of six mobile surfaces and did nothing: the shared
onMenuClick had no rename branch, so only the sessions page — which intercepted the key itself —
ever renamed. Handle it once in the shared hook so every host gets it, with an inline editor on
the row and a cache write that also retitles an open chat tab.

The agent roster and Home passed only onOpenOverview, so AgentCard computed hasMenu false and
dropped the kebab entirely. Wire the three verbs to the useAgentActions hook /m already uses on
the overview, and add the automation menu entries and the overview links that were simply never
passed.

* chore(frontend): refresh the generated next-env types path

Next 16 emits its route types under .next/dev; the generated file follows.

* feat(frontend): bring the mobile composer up to the desktop

Two gaps, both because /m never used plumbing that already existed.

Unsent text was lost on every tab switch: /m routes per session, so the whole composer subtree
unmounts, while /w survives by never unmounting. The per-session draft map and the send-side
delete were already shared — /m just never read or wrote them. Move useComposerDraft into
@agenta/chat/hooks (revealPlayedRef made optional) and pass initialMarkdown and onChange.
Closes #6541.

Slash commands were missing for the same reason: /m renders the same RichChatInput, which owns
the palette, and simply never received slashCommands. Move useChatSlashCommands, the permissions
panel and its roving-list engine into @agenta/chat, injecting the two oss-only bits
(newSessionLocked, onNewSession) as optional props so /new hides where no host supplies them.

Also take the shared CHAT_COLUMN in ContentRail, so the transcript stops capping a step narrower
than the desktop reading width.

* fix(frontend): stop clipping config row summaries, and square the region bars

The summary cap was a percentage, and it resolved against the trailing cluster
— which is shrink-to-fit, so the cap measured the text against its own width.
Every summary lost roughly the same 45%, clipping even "1 file" to "1 f...".
The title is the half that gives way now; preserveTitle still swaps that round,
which its test pins.

The region wrappers also kept the w-full their header bars just dropped.

* feat(frontend): make the two side panes alternate on /m below the coexistence width

/m had no coexistence rule, so both side panes stayed open at any width from md up. With the
shared pane bounds that squeeze is measurable: at 1400px the Files pane sits pinned at its 420
min with no drag range, and the conversation drops under its own floor.

Mount the shared threshold, and below it evict the other pane the way the desktop does — the one
you just opened wins, and shrinking past the line keeps Files. Gated on the two-pane layout:
below md only one pane ever renders, so evicting there would drop state the user would find
missing when they widened the window.

Also mount the session shortcuts, and reveal the rename pen on hover rather than always: /m now
serves desktop, and the pen stays visible only where there is no hover to reveal it.

* fix(frontend): give the skill drawer a phone drill-in

The file rail held a fixed 176px whatever the width, leaving the editor beside
it a sliver on a phone. Below sm the two panes now take turns the way the
version history drawer does: the list fills the drawer, picking a file pushes
the editor, and a back link returns. Adding a file drills straight into it.
Desktop geometry is unchanged.

* feat(frontend): drop the Edit/Preview switch from the instructions drawer

The editor already renders markdown live, so Preview showed the same thing read-only behind an
extra control. Always edit.

* fix(frontend): scroll the editor toolbar instead of wrapping it on a phone

The button row is wider than a phone drawer, so it wrapped onto a second row and
took vertical space the editor needs. Below sm it is one row that scrolls
sideways, with the buttons holding their size rather than compressing; from sm
it wraps as before.

* style(frontend): cut the repetition out of the integration drawer header

"Integration · gmail · gmail-main connection" under a bold "Gmail" said the name twice and
labelled what the logo already shows. Put the slug beside the name in the same faded weight, drop
it where the name IS the slug, and leave the connection on its own line.

* fix(frontend): keep the model picker inside a phone screen

The cascade asks for a fixed 400px, so on a narrow viewport it hung off both
edges. The popover is now capped at the width Radix measures between the
viewport edges, and the panel shrinks with it.

Inside that clamp the 200px provider column left the flyout too little and every
model name truncated, so below sm the column drops to 132px. "Manage model
providers" cannot fit a column that narrow without wrapping onto three lines, so
on the same breakpoint it moves out from under the column to sit beneath the
cascade, where it has the popover full width.

* feat(frontend): drop the Form/JSON toggle from the config item drawer

An item either has a structured form or it does not, and the drawer already knows which — jsonOnly
covers bare builtin and provider tools and connection entries. The toggle only let a reader put a
form-backed item into raw JSON, which is not an edit anyone needs.

Remove it and everything that existed only to feed it: the ConfigItemView type, the view and
onViewChange props, the drawerView state, formOnly (with no toggle to hide, it was just the
default), and editView, whose logic folds into jsonOnly. The view argument threaded through
openCreate/openEdit goes with it.

* style(frontend): tighten the phone session list and swap its filter sheet for a popover

The bottom sheet spent a full-height overlay and its own header on three short
facets; a popover anchored to the trigger closes on the same tap outside. The
rest is density at phone width: the Filters trigger, the status chips, the
toggles and the page title all drop a rung, and the row's pin hides below `sm`
since the `...` menu already carries Pin/Unpin.

* style(frontend): hide the Observability entry from the mobile rail

Behind a constant rather than deleted — the screen, its route and the entry itself all stay, so
showing it again is a one-line flip.

* fix(frontend): give the integration catalog a phone drill-in

The category rail held a fixed 176px whatever the width, leaving the app list a
sliver on a phone. Below sm the two take turns the way the skill drawer does,
except the apps open first - they are the point of the drawer - and the rail is
a tap back. Picking a category returns to the list, and the back link names the
category in scope. Desktop geometry is unchanged.

* fix(frontend): size an icon-only Button square by default

The icon size variants existed, but call sites kept omitting size — data-table renders its reload
button as a bare outline Button holding one glyph, so every table that offers reload got a button
as wide as a labelled one. Infer the icon size from a single element child with no text.

Only inferred when size is absent, so passing any size (default included) still wins, and skipped
under asChild, where the child IS the button rather than an icon. Text buttons are unaffected:
their children are strings.

* fix(frontend): give the tool name room in the permission drawer on a phone

Three things crowded it out. The per-tool permission select held a 132px floor
at the drawer body text size, so a tool read as "Product S..."; below sm it
drops to 104px and the smaller size, and its option labels and helper lines
follow. The drawer title carried the integration slug at shrink-0, so the slug
survived intact while the name it belongs to was cut to one letter - it is
dropped below sm, where the name and connection already say it. The title logo
steps down to 18px on the same breakpoint.

ProviderLogo takes a className for that: `size` renders as width/height
attributes, so a responsive override has to come from CSS.

* fix(frontend): make the select list readable in dark mode

Two states were invisible on a dark panel. Hover painted bg-muted, which resolves to #242424 in
dark — the exact colour of the dropdown it sits on (in light the two differ, which is why it only
showed there). Use controlItemBgHover, the translucent overlay the file comment already named.

The checked row painted bg-controlItemBgActive, which /m never generated: the token was in oss
tailwind config but absent from the mobile bridge, so the utility silently emitted nothing and the
selected option looked like every other. Bridge it, and add controlItemBgHover to the oss config,
which was likewise missing it.

* fix(frontend): drop the subagent row meta line on a phone

The model slug and connected-app logos took a full line under each subagent and
still truncated, crowding out the name and description the row is actually
picked by. They are desktop-only now. The empty state beside them also kept its
own line rather than wrapping onto three, which still applies at sm and up.

* style(frontend): cut redundant chrome from the config panel rows

Four things the rows said twice or said in a fixed light colour:

- The "skill" tag on every skill row, inside a section already headed Skills. The other skill
  tags stay: static, a version, @ag.embed all say something the reader cannot otherwise tell.
- The "Always asks" permission summary on integration rows.
- "Markdown ·" before the word count, next to a filename ending in .md.
- The instructions file row bordered with --ag-c-EAEFF5, a fixed light hex, so its edge vanished
  against a dark section. Use the same border and ground as the integration and subagent rows.

The integration drawer header also folds onto one line — name, slug, connection.

* fix(frontend): give the source catalog a phone drill-in

The 150px connections and categories rail left the apps grid a sliver on a
phone. Below sm the two take turns: the grid opens, since browsing apps is what
the page is for, and the rail is a tap back. Picking a connection or category
returns to the grid, and the app detail keeps its own Back on top of that.
Desktop geometry is unchanged.

* style(frontend): fold the drawer headers onto one line and drop their icons

The config item and add-subagent drawers stacked a title over a muted subtitle and led with an
icon that repeated the row the drawer was opened from. Put both on one line, divided by a middot,
and drop the icon — which takes ConfigItemDrawer icon prop and SubagentHeaderIcon with it, since
nothing else used either.

Also match the trigger row corner radius to the skill and integration rows.

* style(frontend): drop the agents roster and overview a rung on a phone

The agents surfaces still wore desktop geometry at 375px: a 24px page title, a
full-size create button, 40px card gutters and a 28px identity chip. Below `sm`
they now match the sessions bar and the playground header instead. The row pin
hides there too, as it already does in SessionRow, and the overview kebab hangs
from the right edge it sits against — via a new `align` prop, so the desktop
header keeps the menu it had.

* style(frontend): shrink the trigger actions kebab to icon-sm

At the icon size it was a 32px control on a 2-line row; icon-sm matches the row height.

* fix(frontend): let the config card's model row state just the model

The row summary read "openrouter/deepseek/deepseek-v4-flash · Pi core" on one
unbreakable line, so it overran the row and pushed its own title off the left
edge. The harness goes — it is the half nobody checks here — and the model
truncates via `preserveTitle`, the knob the Advanced row already uses. The
title follows the summary down to "Model", which is what the playground's own
section is called.

* fix(frontend): put the pending session tab last and call it New session

The placeholder for an active session the list does not hold yet rendered before the rows, so a
just-created session appeared at the head of the strip while every later one appends at the tail.
Render it last, and label it New session rather than This session.

* fix(frontend): keep the table toolbar actions on a phone

The toolbar dropped its primary actions below sm whenever an empty state was on
screen, on the grounds that the empty state carries its own call to action. On a
populated table that reasoning does not apply, and the settings tables were left
with a lone reload button and no way to act. The actions are back at every
width.

They also stretched full-width and stacked. They now keep their natural width at
one size down, right-aligned, with reload matching - so the toolbar reads as one
row of controls rather than a column of blocks.

* style(frontend): divide the session tabs with a hairline and dot the pending one

SessionTab documents itself as "plain labels on the canvas, separated by the host's hairline
divider" — the host never drew one, so the strip read as evenly spaced text. Draw it in the gap
left of each tab, suppressed either side of the filled active chip, which separates itself.

The pending tab also had no status dot, so it was the one chip in the row missing its leading
glyph. Give it the shared idle presentation, exported by status rather than re-hardcoded.

* refactor(frontend): put the Advanced drawer on a section rail

The three Advanced groups were stacked accordions in an 880px drawer, so
auditing a configuration meant opening and closing them one at a time.
Design 1a replaces them with the same section rail the tool and trigger
drawers use: Permissions / Execution / Build kit, one panel at a time, in
a 560px drawer.

RailField gains a stacked layout, selected through a context so the
sandbox, Claude and Pi permission controls keep rendering their own rows
unchanged. The build-kit block drops its accordion chrome for a plain
panel with the enable switch in its header.

* style(frontend): drop the fade above the mobile composer

A 28px gradient sat between the transcript and the composer, reading as a shadow on an otherwise
flat surface. The desktop keeps its own fade, which is a different thing: that one is inside the
transcript at bottom-0, dissolving scrolled content, while this sat outside the scroller above the
footer.

* style(frontend): pare back the Advanced drawer's chrome

Drop the header icon, the "Draft — applies on save" footer note and the
Build kit rail's amber dot: each restated something the drawer already
shows, and together they crowded a panel whose job is to stay quiet.

Run the rail divider the full height of the body so it meets the header
and footer rules instead of floating between them, and close up the
space around the rail — the drawer's side inset drops to 12px (the
vertical 16 stays, since that is what the divider's bleed negates), the
gap to the divider to 8px, and the toggles' own side padding to 8px.

The footer note came off SectionDrawer's default, so the Model drawer
loses it and takes the same insets.

* feat(frontend): surface the keyboard shortcut hints on /m

The registry defines 41 shortcuts; 11 carry an on-screen hint somewhere and /m was missing or
suppressing five of them. Add the keycap and aria to the config-panel collapse, the aria to the
two pane controls that had a keycap but no accessible name, and the keycaps to New session, Close
and Archive.

Three land in packages so the fix is not /m-only: the menu-row keycap layout was private to the
desktop tag bar and is now shared as withShortcutKey, the rail owns its own New/Close hints, and
ApprovalCard gated its keycaps on `touch` — which conflated "sized for fingers" with "has no
keyboard" — so it now asks useHardwareKeyboard, as the composer already did.

Rename and Search stay bare on /m: Alt+R opens nothing there and Alt+K toggles the pane rather
than focusing a search box, so a keycap would advertise a dead key.

* fix(frontend): give the chip inputs a real suggestion dropdown

ChipsInput offered its suggestions through a native <datalist>, which
draws an unstyled OS popup, opens on the browser's terms rather than the
field's, and can't be driven from our own keyboard handling. The box
also reserved 24px on the right for a caret it never drew.

Build the list here instead, on the Popover + role=listbox +
aria-activedescendant pattern Combobox already uses: it opens on focus,
filters as you type, hides values already chipped, and takes arrows,
Enter and Escape. Free text still chips on Enter — the options are
suggestions, not a closed set — and the caret (with its inset) now
appears only where there are suggestions to show.

The input lives in the popover's ANCHOR, so every click and focus inside
the field reads as an interaction OUTSIDE the layer and would dismiss
the list on the tail of the very click that opened it. Guard both
outside paths on whether the event came from the field, and focus the
input by hand off the box chrome rather than letting the browser do it
mid-gesture. Escape is swallowed while the list is open so it closes the
list and not the drawer around it.

Shared by the permission fields, SchemaForm and ParameterNodeEditor.

* feat(frontend): rename a session from its tab on /m

The tabs had no inline rename: no pencil, no double-click, and the context menu Rename was a
silent no-op because the shared onMenuClick has no rename branch. Give the rail the inline editor
the session ROWS already use — same useInlineRename, same InlineRenameInput, same commitRename —
so a rename from a tab also retitles the open chat.

SessionTab only mounted its actions while hovered, so a hover-revealed pencil would never exist on
touch. It now mounts on (hover: none) too, and the pencil hides only where there IS a hover.
Desktop is hover:hover, so its mount rule is unchanged.

That closes Alt+R, which was mapped to a no-op for want of this editor, and the menu row can name
its key alongside Archive.

* style(frontend): settle the config row title, the build-kit switch and its caption

The item row's name was semibold at 13px, which read as a heading in a
list of them and competed with the panel titles above it — drop it to
normal weight.

The build kit's enable switch was the default size while every per-tool
switch under it is small, so the block's master control looked heavier
than the rows it governs. Match them.

Cut the build-kit caption from three lines to two: the heading already
says "playground", and the tools list below says what the kit holds, so
the caption only has to place the tools and say they never ship.

ItemRow is shared, so the tools, subagents, skills and MCP lists take
the lighter name too.

* perf(frontend): stop the whole transcript re-rendering on every commit

Measured on a 12-turn session: 20 commits produced 644 TurnRow, 460 AssistantMarkdown and 276
ToolLine renders, and every single commit was a long task — median 67.6ms, p95 90ms. Cost grew
with the transcript at 13ms + 4.7ms per turn, so a 100-turn session would block ~480ms per commit.
Streaming commits at roughly 20/s, so this is the hot path.

Three causes: turn view models were rebuilt for every turn on every commit, TurnRow was not
memoized, and handleRewind closed over the whole conversation object, which is a fresh literal
each render.

Cache view models by message identity, memo the row, and depend on rewind alone. Same session
now: 0 row renders across those 20 commits, median 18.2ms, nothing over 50ms, and the cost is
~17ms flat regardless of length. The cache is opt-in — a caller passing none behaves exactly as
before, which is what the desktop does.

Also move six call sites off the @agenta/playground root barrel onto the agent-chat subpath. That
one is hygiene, not a win: the bundle is unchanged because the graph arrives on this route anyway.

* style(frontend): widen the Advanced rail to 148px

At 132px the rail's labels sat tight against their icons with the column
ending well short of the panel it introduces. 16px more gives the three
labels room without taking a meaningful bite out of the panel.

* fix(frontend): make /m usable on a phone

A small-device audit of every screen except Observability found content starved or clipped at
320-414px. Each fix reverts at sm or lg, so the desktop is unchanged.

- Settings: 64px gutters left a 192px column on a 320 screen, so titles read "API ..." and
  descriptions ran eight lines. Gutter is now 16px until lg, which buys 96px on every tab.
- Billing: "Cancel subscription" ended past the viewport edge and could not be tapped. The plan
  action row wraps.
- Templates: no horizontal gutter at all, and a 226px vertical stack of category chrome above any
  content. Gutters added; categories scroll horizontally until lg, worth ~190px of height.
- Config sections: the label collapsed to nothing against a 220px summary. Cap the summary at
  120px until sm.
- Transcript: assistant turns gave up 44px a line to an 85% cap that only user bubbles need.
- Tool lines: the argument rendered at 0px, so the command was invisible. Cap the label instead.
- The rename pencil was a 13px tap target; a transparent hit extender makes it ~31px.

* fix(frontend): let the keyboard open a changed field's detail

The changed-label trigger was a span with role="button", which sets the
ARIA contract without making the element focusable. Keyboard users could
neither open the committed-value popover nor reach its Restore button.

Use a real button, stripped to the label's own type and colour, with a
focus-visible ring so it shows where the focus lands.

* fix(frontend): stop the session row title collapsing on a phone

The chip, the agent name, the timestamp and the pin are all shrink-0, so the title — the only
part that identifies the row — got 0px at 320 and 6px at 375. Hide the 96px agent-name column
below sm; the agent overview already renders this list without it.

Also widen the pin hit target on touch, where there is no cursor to aim with.

* chore(frontend): cut the new comments down to one line

web/AGENTS.md sets a hard one-short-line rule for in-code comments, and
this branch added several multi-line blocks narrating why. Trim them to
a line each, keeping only the two that record a real trap: the popover
anchor Radix reads as "outside", and the drawer padding the rail's bleed
negates.

* fix(frontend): mount EntityModalsProvider on /m

Nothing under web/mobile imported @agenta/entity-ui/modals, so the provider was never mounted.
Nothing breaks today — the three modals are lazy and atom-driven, and no shared component
dispatches yet — but useEntityActionDispatch THROWS outside the provider, so the first drawer
ported to /m would fail at runtime with nothing to point at.

* fix(frontend): give the drawer section rail a phone layout

The rail held a fixed column and a 16px gutter whatever the width, so a narrow
drawer squeezed the panel beside it to a sliver. Both step up at sm, and the
Advanced drawer opts into a drill-in: the section list opens, picking one pushes
its panel, and a back link returns.

Drill-in is opt-in rather than the rail default - the commit modal, the version
axis and agent-home templates share this component, and a short two-item axis
reads better as a plain toggle.

* fix(frontend): seed the project id before the first render on /m

ContextSync and useBindProjectContext bind projectIdAtom in an effect, which lands a commit after
render, so every query mounted in that first commit keys on a null project and mints a second,
permanently disabled cache entry. A session screen carried 10 of them.

Seed the atom from the URL at module load instead. Binding during render would notify the
already-mounted ProjectWatch mid-render, which React warns about; module scope runs before
anything is mounted.

This removes dead cache entries, NOT requests — the enabled guard already stopped the null-keyed
twins from fetching. Both files are mobile-only, so no desktop key or guard changes.

* style(frontend): drop page titles and their controls a rung on a phone

Settings and Home carried desktop type and control sizing straight onto a phone.
Page titles step down to the 16px body ramp below sm and back to 24px from sm,
the settings docs link moves onto the title row so it stops wrapping under the
description as a third line of body copy, and Home new-agent takes the small
control size. Regenerated shadcn tokens come along.

* perf(frontend): stop rebuilding the config pane on every toggle

Toggling swapped ConfigPane and SessionsPane by unmount/mount, so the whole schema-form surface
was rebuilt each time: 24 toggles measured 445ms median to the counterpart appearing, worst 778ms,
10.9s total, and 32 API requests from the remount refetch storms. Keep ConfigPane mounted and
display:none it instead, as the desktop does with its panes. Steady state is now 10ms median,
0 requests; only the first open pays a mount. SessionsPane stays on unmount — it is cheap and
carries a 15s refetchInterval that should not run behind a closed pane.

Also make document.title track a rename. Adding the key to NAMED_SESSION_QUERY_KEYS was not
enough on its own: commitRename patches by key PREFIX from index 0 and the query led with
"mobile", so nothing matched, and the cached value is a single session where withRenamedSession
only understood lists. Re-key it and teach the patcher the bare-session shape.

And bind the run-level shortcuts the shared SendButton already advertises — Escape to stop while
streaming, Alt+G to approve one gate — so aria-keyshortcuts="Escape" stops naming a dead key.

* chore(frontend): apply prettier to the selection components README

Blank lines between an import and the JSX example that follows it.

* fix(frontend): make the drawers and modals usable on a phone

An earlier small-device pass checked screens but never opened what sits on top of them. Auditing
the overlays found two root causes behind most of it:

- EnhancedModal set maxWidth to its own width with no viewport clamp, so a 520px modal hung
  ~100px off BOTH edges and its close button was unreachable. One line fixes every modal.
- Every drawer header used a bare flex-1 title. A flex child defaults to a min-content floor that
  truncate cannot defeat, so a nowrap title row measured 445px inside a 320px panel. One min-w-0
  fixes every drawer, and the same cause explains the config drawer footer note.

The rest are fixed-width rails and grids that assume desktop room: the tool parameter tree left a
79px detail pane, the catalog drawer put 29 nodes past the panel edge, the tool selector popover
was wider than any phone, and the RailField 116px label column starved every form body. Rails are
narrowed rather than hidden so trees stay navigable, and RailField stacks its label below sm.

The session automation chip was shrink-0 against a flexible title, leaving it ~10px at 320.

Every edit is either an sm-prefixed restore of the original value or inert until content
overflows, so the desktop is unchanged — measured in Storybook, which renders the same
components: the config drawer went from 5 nodes past the edge to 0, the catalog from 29 to 0.

* fix(frontend): resolve the review findings on the mobile parity work

Two real bugs. The agent resolve was keyed by project so nothing remounted per
session, which meant a list cached before a session existed would never be
re-read - opening a shared session link with no ?agent= left the composer
disabled for the full staleTime. It now re-reads once per missing session, and
holds the resolving state while it does. The drawer section rail made its
divider and gutter sm-only, which suited the drill-in but stripped both from the
three call sites that still show rail and panel side by side on a phone.

Three cleanups. The affordance column and its size constant were exported
through two barrels with no consumers, so they are module-private again. The
region bar geometry was a hand-maintained copy of the Configuration header, and
the w-full fix had to be applied to both - one exported constant now, each host
keeping only its own fill. The model picker footer was rendered twice and
toggled by CSS; it renders once, width-matched to its column.

Also cut the comment blocks that ran past the one-line rule.

* refactor(frontend): make the agent icon, name and rename one shared control

Both /m surfaces hand-assembled the same identity: SessionTopBar built a chip
and passed it plus AgentNameInline into AgentPageHeader, while the agent
overview had the icon picker but a plain heading and no rename at all.

AgentIdentity owns the chip geometry per size and composes the picker popover
with the inline rename, so a host passes a size rather than classes.
AgentIconPopover moves into the package (the lazy picker import survives - it
keeps the virtualizer off every screen) and AgentPageHeader gains an identity
slot that replaces its icon/name pair.

The overview page gains inline rename as a result, which is a real behaviour
change: rename writes to the server, so the stale comment claiming the icon was
the one editable thing because it is a local display preference is gone.

* refactor(frontend): apply the simplify-pass findings

Two claims in the code were false and are now true. TurnRow carries a memo whose
comment says it holds because the view models stay identity-stable - but the
read-only chat screen never passed the cache that makes them so, so the memo
never hit; it does now. And the snapshot cap said it matched the tab rail
open-tab cap, which is 50, not 12 - the number was anchored to nothing, so the
comment now says what it actually is.

CONFIG_REGION_BAR moved from entity-ui to @agenta/ui beside ConfigRowTrailing:
it is pure presentation, and leaving it where it was made a deliberately light
component pull the whole drill-in barrel for one string.

Dead code: three transcript ref seeds the mount effect overwrites before anything
reads them, a data-table condition that cannot be false inside its own branch,
two single-use SectionRail booleans, and a CatalogChooser rail width no
breakpoint ever applies - which also retires an !important. Trimmed the
ConfigRowTrailing comments to the constraints that are actually surprising.

* style(frontend): give every session tab the same width

Tabs sized to their label between 112px and 180px, so the strip read as a
ragged row. One fixed 180px width covers real tabs, pinned ones and the
trailing New session placeholder, since all three render through SessionTab.

* fix(frontend): stop prettier and the token generator fighting over theme.generated.css

CI's TypeScript lint failed with "theme.generated.css is stale". The file is
generated and must not be hand-edited, but a `pnpm lint-fix` run had let
prettier rewrap its shadow values across several lines. The generator emits them
on one line, so `generate:tokens --check` saw a mismatch. Prettier and the
generator each wanted the other's output, and whichever one was committed broke
the other gate.

The generated file is now prettier-ignored, which is what "never hand-edit this"
means in practice, and the committed copy is the generator's own output again.

* perf(frontend): stop refetching the whole session list once per session opened

useAgentEntity resolved a session as agent by fetching every session in the
project, then finding one row on the client. The response is identical whichever
session asks, but the cache key carried the session id, so opening N sessions
meant N full-project fetches held as N separate entries.

Keying by project alone collapses those to one. The row is picked in render,
which needs no cache entry of its own.

A freshly minted session also sat in its loading state waiting on that list,
which by definition could not contain it - the route already names its agent
via ?agent=, so resolving no longer waits on the list when it does.

Switching sessions also blanked the transcript to a skeleton and re-read it,
even returning to one just left. The last adopted transcript per session is
kept (capped at the tab rail 12) and seeded on switch; the load still runs and
adopts anything newer. A seeded transcript no longer falls through to the empty
state, since an unchanged record log correctly declines to adopt.

* fix(frontend): centre the project switcher in a collapsed /m rail

DrawerProjectSwitcher hardcoded collapsed={false}. That is right in the drawer,
which never collapses, but the same component also fills the docked rail's
afterBottom slot, so at 48px it kept its expanded geometry: w-full, gap-2 and
px-3. The padding put the avatar 8px right of the icon column's axis, left the
row 46px tall against the nav rows' 32px, and still rendered the project and
org labels the collapsed rail has no room for.

SidebarShell already hands every slot the rail's state; both AfterBottom slots
were dropping it. They forward it now.

* chore(frontend): apply lint-fix formatting to two untouched files

Import order and indentation only, picked up by a repo-wide lint-fix run.

* fix(frontend): resolve the review findings on the small-device pass

Five of the seven review comments held up.

The chip input's suggestion list auto-highlighted row 0 and reset to it on every
keystroke, and Enter committed the highlight. Typing "Bash" therefore chipped
"Bash(*)", so no typed value that merely prefixes an option could be entered,
against the field's documented contract. Nothing is highlighted now until the
user arrows or hovers onto a row, and Enter takes the draft otherwise.

Importing or pasting a skill selected it without leaving the file list, so on a
phone the editor it had just filled stayed off screen. Both paths go through
openEntry.

The phone pin hid unconditionally on the claim that the row menu carries
Pin/Unpin. That menu is optional and its entries need not include "pin", so a
host without it lost the action entirely. The button now hides only where the
menu actually offers it.

ItemRow is exported from the drill-in barrel so the alignment story imports it
by package rather than by relative path, and ChatScreen's deliberate [sessionId]
deps carry a scoped disable instead of two standing lint warnings.

* fix(frontend): import the moved helpers from their packages, not through oss

The extractions left compat shims in oss that re-export from @agenta/*, which
the oss lint bans for tree-shaking. Deleted the two pure re-export files and
pointed their importers at the packages; menuEntries keeps its own adapters and
loses only the re-export line. Also formats a chat test the push hooks skipped.

* perf(frontend): stop the liveness tick re-rendering the whole playground

ChatScreen owns the config pane, its drawers and the transcript, and it read
liveness.dataUpdatedAt. React Query only re-renders on properties a component
actually accesses, and that timestamp moves on every poll tick even when the
payload is byte-identical - so the entire subtree re-rendered on a timer.

Only useAgentConversation reads it, for the remoteRunIsFresh comparison, so
LiveConversation subscribes to it directly. Same query key and cache entry, so
the value and the request count are unchanged.

Partial: this removes SessionWorkspace as an idle re-render source, but idle
commits continue at a similar rate from a second driver in the sidebar that I
have not identified yet.

* fix(frontend): strip only the pinned marker in the tab test helper

CodeQL flagged `spec.replace("*", "")` as incomplete sanitization: it drops the
first `*` anywhere in the string, not the trailing flag the helper means. Not
exploitable in a fixture, but the code now says what it does.

* fix(frontend): let the model picker footer span the panel

I had width-matched it to the 200px provider column it used to sit inside, which
put "Manage model providers" on two lines. It takes the panel width now, so the
label reads on one line at every size.

* fix(frontend): resolve the review findings on the parity branch

Escape aborted the local stream only, so the transcript looked stopped while the
run continued on the server and the next revalidation adopted it back. It routes
through the same stopHere path the composer Stop uses. The listener reads its
volatile inputs through a ref too, since pendingApprovals is rebuilt on every
streamed commit and was re-registering the handler on the hot path.

Inline rename could commit twice - Enter and the blur it triggers - and the two
could resolve out of order and undo the later name. It is now single-flight.

Both session tab atoms hydrate with getOnInit, matching the convention the
sidebar order atom already documents. Without it they read {} until storage
loads, and the seed effect could overwrite persisted tabs with the first server
list.

Transcript snapshots are cleared on logout: it is a client-side route change, so
a module-level cache outlives it and a reused session route could paint the
previous account transcript.

The route-scope test moves to the package the function now lives in, which also
fixes the CI failure from deleting its old re-export shim.

* fix(frontend): stop the collapsed rail cropping the project switcher

The switcher passed its own wrapper as the popover container. In the nav drawer
that is right - without it the panel renders behind the sheet - but the same
component feeds the docked rail, where the wrapper sits inside a 48px
overflow-y-auto column. An overflow ancestor clips a portalled child like any
other, so the 220px panel showed as a 48px sliver with every label cut off.

The container is now chosen by context: the sheet when there is one, otherwise
null so Radix portals to the body. Measured in the app - nearest clipping
ancestor none, portal parent BODY, visible width 220px.

* refactor(frontend): drop the duplicated mobile select and popover primitives

The popover copy was dead: AgentIconPopover moved to @agenta/ui during the
identity extraction and the 41-line shadcn file was left behind with no
importers.

Select had three call sites and the package version is a superset - same
exports plus selectTriggerVariants, sizes sm/default/lg against mobile sm/default,
and no antd vars. The one behaviour change is SelectContent defaulting to popper
rather than item-aligned positioning, which matches the desktop.

Sheet stays where it is deliberately. Mobile has a `responsive` side - a bottom
sheet on a phone, a right-edge drawer from lg - that ten settings sheets use and
the package has no equivalent. Adding it there would push one app layout policy
into a primitive oss and ee also render, and its `bottom` variant is an antd
Drawer port (fixed 378px, drawer animations, square corners) that a call-site
className would have to fight rather than compose with.

* chore(frontend): cut the new comments to one line

The three comments added with the collapsed-rail fix ran to two and three lines,
against web/CLAUDE.md's one-line rule. The ItemRow note goes entirely: the export
list already says it is public.

* chore(frontend): resolve the remaining review threads

The token generator reached oss through a relative cross-package path, which
AGENTS.md bans; it uses the @agenta/oss subpath now and tokens:check still
passes. Three comment blocks cut to the one line the rule allows.

* perf(frontend): mount a session row menu on first press, not on mount

Every rail row mounted a complete Radix DropdownMenu - root, Popper, PopperAnchor, MenuProvider, Presence and portal providers - for a menu behind a kebab that stays invisible until hover. With 25 rows that was 54 DropdownMenu, 78 Popper and 86 Presence fibers on a session page.

The row now renders a plain button until the first press, then swaps in the real menu. Arming is on pointer-down, matching when the Radix trigger would have opened, plus Enter and Space for the keyboard. Down to 4 DropdownMenu, 47 Popper and 55 Presence.

* fix(frontend): mark live-preview reasoning as streaming so thinking shows

applyFrame gave tool parts an explicit state but left text and reasoning parts without one. Both renderers gate the thinking block on exactly that field - isLiveReasoningPart on mobile, and the desktop AgentMessage - so it was always false and a live block rendered as a collapsed, settled Thought instead of an open one.

reasoning-start and reasoning-delta now mark streaming, reasoning-end marks done. This is the watch path, which is what both surfaces render a running session through; a session driven from your own tab was never affected, since the AI SDK sets state itself.

* style(frontend): size a session tab to its label between a floor and a ceiling

Every tab was a fixed 180px, so a short name carried the same width as a long one. It is now label-sized between 120px and 180px: the floor keeps a clickable zone left of the hover actions, which overlay the right 58px, and the ceiling keeps one long name from eating the strip.

* fix(frontend): keep a just-created session on the tab rail, and reveal it

A session the capped list does not carry yet had one stand-in chip, rendered only while it was the active session. Navigating away left it with nothing to render as, so a new blank session dropped off the rail even though its id stayed in the open set.

The rail now keeps its own set of unlisted tabs and renders a chip for each, dropped as soon as the list carries it or the tab is closed. There is no row view-model behind such a chip, so it offers no menu and no rename; hosts route it from the id through the new onSelectUnlisted, and the close removes it from the local open set only, never off the server.

The reveal that keeps the active chip in view moved into a hook these chips use too. It was previously inside RailTab alone, so a new session - whose chip is appended last, past the right edge of a full strip - was never scrolled to. It now measures over a few frames and scrolls instantly, because the inline New session button pins itself after the first measure and left the scroll 32px short.

* v0.115.4

* refactor(frontend): put the mobile nav on the phosphor icon set

The rail drew lucide glyphs while every other Agenta surface draws phosphor, so
the same nav entry had two different icons depending on which app you opened.

* feat(frontend): give the composer palette a /new row

The `/` palette had no way to start a session, so the rail's `+` was the only one
on /m while the desktop offers both. The row is opt-in and gated the same way the
rail's `+` is: starting a session needs an agent, so without one the palette omits
it rather than offering a dead entry.

* chore(frontend): fix the import order in SessionTabs

* fix(frontend): key the turn cache on the client-tool classifier

The cache compared message identity, a derived key string and the executed set, but not the predicate that decides client-tool classification. That predicate reads renderMap, which is built across the WHOLE conversation, so a data-render hint arriving on a later message could reclassify an earlier turn while its message object was unchanged - the cache returned the stale view model and the widget rendered as a plain tool line.

The caller now memoizes the predicate on its own inputs and the cache compares its identity, so the memo dependency the hook declares reaches the cache. Covered by a test that pins both halves: identity holds while the predicate is stable, and a new predicate rebuilds the turn.

* refactor(frontend): drive the composer's trigger menus from one palette plugin

The `@` file menu needs the same Lexical mechanics the `/` menu already has. A
second plugin would claim Enter at CRITICAL alongside the first and race by mount
order even while closed, keep a divergent dismissal latch, and clobber the same
aria-activedescendant on the one contenteditable root. So the plugin now takes N
palette specs and owns the keyboard once.

The `/` palette's behaviour is unchanged: slashCommands.ts keeps every export and
its unit test passes unedited. Also moves @floating-ui/react to dependencies, since
mobile is about to take a runtime path through it.

* feat(frontend): mention drive files in the composer with @

Typing `@` opens the same palette `/` opens, listing the session drive: recents
and the root to start, type to search the whole drive, Tab into a folder, Enter to
reference a file or a folder. The reference goes in as an inline-code drive path,
which is what the sent message already renders as a file chip.

Inline code rather than typed backticks because $convertToMarkdownString escapes a
backtick in unformatted text — a plain-text path would…
pull Bot pushed a commit to bhardwajRahul/agenta that referenced this pull request Sep 9, 2026
Classic mode off has meant a simplified desktop sidebar. It now means the
simplified APP: those users land in /m for every page /m has, and the desktop
keeps the rest.

Nobody is opted in by this commit who was not already opted in by signup —
setNavSimplifiedDefault(true) has fired for every new user since 2026-07-28
(Agenta-AI#5478), and existing users default to classic on and stay where they are.

Built on the device gate rather than beside it, so there is one route map and
one exception list. mapDesktopToMobile stops sending every project route to the
sessions list and maps per route; the new mobileRouteFor returns null for pages
/m does not have (evaluations, test sets, prompts, evaluators, annotations),
which the preference gate reads as "leave them on desktop" and the device gate
still reads as "/m root" — a phone cannot use those pages either way.

The desktop playground maps to a session's chat, since /m has no playground
route: the session page IS the playground there, and ?agent= names the agent a
session with no turns cannot name for itself.

Two layers, because the preference is localStorage and the middleware can only
read cookies: the apps mirror it to agenta-classic-mode, and the first visit —
which has no cookie yet — is caught in the client instead. An absent cookie is
UNKNOWN, never "off": the preference is per-browser, so guessing would move
users who never chose the simplified experience.

The client hop waits for a settled session and never fires from /auth. Post-auth
sets the user id and the signup default and THEN routes onward, so a user is
briefly known while still on the sign-in page; jumping apps there races the
router and skips a new EE user's post-signup survey outright.

Leaving is the switch in /m's Settings > Preferences, which carries the user to
the desktop equivalent of the page they were on. There is deliberately no
reverse gate — a Classic-mode user who opens /m stays there.

AGENTA_CLASSIC_MODE_GATE (default on) disables it per deployment, independent of
AGENTA_MOBILE_GATE so device detection is not caught in the same switch.

Also drops web/mobile's declared copy of the gate for the real import, as its
own header asked once /m had workspace deps — the alternative was writing all
of the above twice.
pull Bot pushed a commit to bhardwajRahul/agenta that referenced this pull request Sep 9, 2026
…he browser

The signup-era default was written to localStorage on whichever browser
completed signup, and nothing ever re-seeds it: is_new_user comes from
_create_account, which is idempotent and returns False once a user row exists.
So the same person read "simplified" on their signup browser and "classic"
everywhere else -- and it never healed.

That was survivable while the flag only hid four nav items. It is not now that
it decides which app you get.

GET /profile already returns created_at; the zod schema at the boundary was
dropping it. Let it through, and read the default as "stored flag OR account
created after the cutoff". The signup browser keeps answering exactly as it
always has, and a second device stops disagreeing with it.

The cutoff is padded past the release that started seeding the flag (v0.106.1,
on main 2026-07-30) rather than the merge date of the change itself (Agenta-AI#5478,
2026-07-28), because the seed is written client-side -- what matters is when the
frontend carrying it reached users. Padding forward only ever leaves someone on
the status quo, since the stored flag still covers anyone it misses.

The explicit override stays browser-local. Fixing that needs somewhere to put
it, and there is no user-settings column or endpoint -- the users table is id,
uid, username, email and timestamps. It affects only people who deliberately
flipped the switch, rather than the whole cohort.
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:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants