From c53297809eca49b4b6060621c2953409fb17f071 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 24 Jul 2026 17:17:30 +0600 Subject: [PATCH 1/7] doc: plan the hide pages feature for new user 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 --- docs/design/simplify-nav-new-users/README.md | 40 ++++ docs/design/simplify-nav-new-users/context.md | 85 +++++++++ docs/design/simplify-nav-new-users/plan.md | 171 ++++++++++++++++++ .../design/simplify-nav-new-users/research.md | 127 +++++++++++++ docs/design/simplify-nav-new-users/status.md | 53 ++++++ 5 files changed, 476 insertions(+) create mode 100644 docs/design/simplify-nav-new-users/README.md create mode 100644 docs/design/simplify-nav-new-users/context.md create mode 100644 docs/design/simplify-nav-new-users/plan.md create mode 100644 docs/design/simplify-nav-new-users/research.md create mode 100644 docs/design/simplify-nav-new-users/status.md diff --git a/docs/design/simplify-nav-new-users/README.md b/docs/design/simplify-nav-new-users/README.md new file mode 100644 index 0000000000..feefa21b66 --- /dev/null +++ b/docs/design/simplify-nav-new-users/README.md @@ -0,0 +1,40 @@ +# Simplify navigation for new signup users + +**Status:** Planning complete — decisions locked, ready to implement +**Date:** 2026-07-24 + +New signup users land on the full platform sidebar, which is noise when their only goal is +building an agent. This workspace plans a nav-only simplification in two phases. **Phase 1** +hides the advanced pages (Prompts, the Evaluation group, Overview, Registry, Evaluations) for +new signups. **Phase 2** adds a per-user **"Simplified navigation"** toggle in Settings → +Account so anyone can switch back to the full view to use the LLM-app pages. Existing and +returning users default to the full view throughout. + +## Decisions + +- Two phases: Phase 1 = hide-for-new-signups; Phase 2 = the Settings → Account toggle + (additive — touches no Phase-1 file except one derived atom). +- Nav-only — hide sidebar entries; no route guards, no in-app link changes. +- Hide when simplified: Prompts, Evaluation group (project); Overview, Registry, Evaluations (app). +- Keep always: Home, Agents, Observability, app Playground. +- Default mode seeded by `isNewUser` (new signups → simplified); everyone else → full. +- Stable seam: the sidebar hook reads `isNavSimplifiedAtom` — Phase 1 `= isNewUser`, Phase 2 + `= override ?? isNewUser`. +- No backend, no team-wide enforcement — per-user preference; an invited teammate flips the + Phase-2 switch to match their team. A workspace-level flag is deferred, not blocked. +- Built on the sidebar's existing `isHidden` mechanism; one OSS sidebar file, both editions inherit it. + +## Deliverables + +- [context.md](./context.md) — problem, phased scope, out-of-scope, product language, success criteria. +- [research.md](./research.md) — how the sidebar, new-user state, and settings tabs work today, with `file:line`. +- [plan.md](./plan.md) — the phased, sliced implementation plan (Phase 1: Slice 0–4; Phase 2: Slice 5–6). +- [status.md](./status.md) — locked decisions, open questions, next action. + +## Intended outcome + +A user who has just signed up sees a focused, agent-first sidebar: Home, Agents, and +Observability at the project level, Playground + Observability inside an app. Anyone who wants +the complete platform — an existing LLM-app team, or an invited teammate — flips one switch in +Settings → Account and gets it back, with the choice remembered. No one is ever stuck in the +wrong view. diff --git a/docs/design/simplify-nav-new-users/context.md b/docs/design/simplify-nav-new-users/context.md new file mode 100644 index 0000000000..a4fdf0c9e9 --- /dev/null +++ b/docs/design/simplify-nav-new-users/context.md @@ -0,0 +1,85 @@ +# Context — Simplify navigation for new signup users + +## Problem + +The sidebar exposes the full platform to everyone the moment they sign up. For a brand-new +user whose only goal is building an agent, that surface is noise: Prompts, the whole +Evaluation group, Registry, Evaluations, and the app Overview are pages the agent flow does +not need yet. They add cognitive load and lead to dead-end or empty pages during the first +session. + +The product has pivoted to agent building (see `docs/design/onboarding-revamp/HANDOFF.md`). +The navigation should match that focus for people arriving new — while still letting anyone +who needs the full platform switch to it. + +## Scope + +Nav-only. The entries disappear from the sidebar; nothing else changes. Delivered in two +phases. + +**Advanced items hidden when the simplified view is active:** + +Project scope (`projectItems` in `useSidebarConfig`): +- Prompts +- Evaluation — the entire group (Test sets, Evaluators, Evaluation runs, Annotation Queues) + +App scope (`appItems` in `useSidebarConfig`): +- Overview +- Registry +- Evaluations + +Everything else always stays: Home, Agents, project Observability; app Playground and app +Observability. + +### Phase 1 (this delivery) — hide for new signups + +New signup users (`isNewUser === true`) get the simplified sidebar. Everyone else is +unaffected. There is no switch yet, so a genuinely-new solo user stays simplified until +Phase 2 ships. Invited teammates already read `isNewUser === false` (see research.md §7), so +they keep the full nav. + +### Phase 2 (follow-up) — the toggle + +A "Simplified navigation" switch in **Settings → Account**, backed by a per-user localStorage +override. It flips the mode either way: a new user can reveal everything (e.g. to use the +LLM-app pages), and an existing user or invited teammate can opt into the focused view. Phase 2 +is additive — it changes no Phase-1 file except the one derived atom. + +## Out of scope (both phases) + +- **Route guards.** Direct URLs (`/prompts`, `/evaluations`, `/apps/[id]/overview`, + `/apps/[id]/variants`, `/apps/[id]/evaluations`) still resolve. We only remove the nav + entry points. +- **In-app links.** Buttons or cards elsewhere that navigate to a hidden page keep working. +- **Team-wide enforcement.** The preference is per-user, not per-workspace. An agent team's + invited teammate defaults to the full view and (in Phase 2) flips the switch to match the + team; we do not force every member to the same mode. A workspace-level flag would do that, + but it needs a backend field and is deferred. The override model does not block adding it. +- **A backend field.** No server change in either phase. Everything is client-side localStorage. + +## Product language + +- **New signup user** — a user for whom `isNewUserAtom` is `true`. Set once on first signup, + persisted per-user in localStorage, never cleared today. +- **Simplified navigation** — the reduced, agent-focused sidebar (advanced items hidden). +- **Full navigation** — the complete sidebar (today's behavior). +- **Simplified-nav preference** — the per-user override (`simplifiedNavOverrideAtom`): + `null` = follow the default, `true` = force simplified, `false` = force full. +- **Effective mode** — `override ?? isNewUser`, exposed as `isNavSimplifiedAtom`. This single + value drives both the sidebar and the settings switch. + +## Success criteria + +**Phase 1** +1. A new signup user sees the simplified sidebar (no Prompts, Evaluation group, Overview, + Registry, or Evaluations). +2. An existing/returning user sees the full sidebar exactly as before. +3. Both editions (OSS and EE) get the behavior from one sidebar change — no EE fork. +4. Hidden items never render, auto-open, or become the selected key while simplified. +5. No backend change is introduced. + +**Phase 2** +6. The Settings → Account switch flips the effective mode in either direction and survives a + reload. +7. Toggling the switch updates the sidebar without a full page reload. +8. Phase 1's sidebar behavior is unchanged when no override is set. diff --git a/docs/design/simplify-nav-new-users/plan.md b/docs/design/simplify-nav-new-users/plan.md new file mode 100644 index 0000000000..a15e809971 --- /dev/null +++ b/docs/design/simplify-nav-new-users/plan.md @@ -0,0 +1,171 @@ +# Plan — Simplify nav for new signup users + +Two phases. **Phase 1** ships the hide-for-new-signups behavior. **Phase 2** adds the +Settings → Account toggle so users can switch back to the full view (e.g. to use the LLM-app +pages). Phase 2 is designed to be **purely additive** — it touches no Phase-1 file except the +one derived atom. + +## Scope + +- Nav-only. No route guards, no in-app link changes. No backend. No team-wide enforcement. +- **Phase 1:** new signups (`isNewUser`) get the simplified sidebar; everyone else is + unaffected. No way to switch yet — a genuinely-new solo user stays simplified until Phase 2. + (Invited teammates already read `isNewUser === false`, so they keep the full nav — see + research.md §7.) +- **Phase 2:** a per-user localStorage override + a switch in Settings → Account, so anyone can + force simplified or full. + +## The stable seam + +One derived atom is the sole dependency of the sidebar hook, so the sidebar edits are written +once (Phase 1) and never touched again: + +- **Phase 1:** `isNavSimplifiedAtom = isNewUser` (thin passthrough). +- **Phase 2:** `isNavSimplifiedAtom = override ?? isNewUser` (add the override; same name, same + consumers). + +The hook `useHideAdvancedNav` reads `isNavSimplifiedAtom` in both phases and never changes. + +--- + +# Phase 1 — Hide advanced nav for new signups + +## Slice 0 — Pin the current sidebar with a test + +1. Unit test over `useSidebarConfig` output (or `filterVisibleItems` on the built items) + asserting the five target keys are **present** when `isNavSimplified === false`. +2. Assert non-targets (Home, Agents, Observability, app Playground) are present in both modes. + +**Exit:** a passing test capturing the pre-change sidebar (targets + non-targets visible). + +## Slice 1 — The derived atom + hook + +1. Add `web/oss/src/state/navPreference/atoms.ts`: + + ```ts + import {isNewUserAtom} from "@/oss/lib/onboarding" + import {atom} from "jotai" + + // Phase 1: simplified nav follows new-signup status. Phase 2 adds a user override here. + export const isNavSimplifiedAtom = atom((get) => get(isNewUserAtom)) + ``` + +2. Add `web/oss/src/components/Sidebar/hooks/useHideAdvancedNav.ts`: + + ```ts + import {useAtomValue} from "jotai" + import {isNavSimplifiedAtom} from "@/oss/state/navPreference/atoms" + + /** Advanced sidebar areas are hidden while the simplified view is active. */ + export const useHideAdvancedNav = (): boolean => useAtomValue(isNavSimplifiedAtom) + ``` + +**Exit:** `isNavSimplifiedAtom` returns `isNewUser`; hook compiles and is unit-referenced. + +## Slice 2 — Hide the two project-scope items + +Edit `web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx`. + +1. `const hideAdvancedNav = useHideAdvancedNav()` near the top. +2. Prompts (`PROMPTS_SIDEBAR_KEY`, `:72`): add `isHidden: hideAdvancedNav`. +3. Evaluation group (`evaluation-group`, `:88`): add `isHidden: hideAdvancedNav`. +4. Add `hideAdvancedNav` to the `projectItems` `useMemo` deps. + +**Exit:** with `isNewUser` true, `projectItems` (post `filterVisibleItems`) has no Prompts and +no `evaluation-group`; with it false, both present. Non-targets unchanged. + +## Slice 3 — Hide the three app-scope items + +Same file, `appItems` memo (`:144`). + +1. Overview (`:147`), Registry (`:164`), Evaluations (`:174`): change `isHidden: isHidden` to + `isHidden: isHidden || hideAdvancedNav`. Do **not** touch Playground or app Observability. +2. Add `hideAdvancedNav` to the `appItems` `useMemo` deps. + +**Exit:** with `isNewUser` true, `appItems` (post `filterVisibleItems`) has no Overview, +Registry, or Evaluations, and still has Playground + Observability when the app-context gate +allows. With it false, all five behave exactly as before. + +## Slice 4 — Manual QA (Phase 1) + +1. Run the local stack (OSS + dev per root `AGENTS.md`). +2. New user: set `agenta:onboarding:active-user-id` to the user id and + `agenta:onboarding::is-new-user` to `true`, reload → simplified sidebar. Confirm the + five items gone, Home/Agents/Observability/Playground remain. +3. Set `is-new-user` to `false`, reload → full sidebar returns. +4. Check both the main (project) and workflow (app) sidebars. +5. Confirm no empty section header / stray divider where the Evaluation group was + (`filterVisibleSections` drops empty sections — verify visually). + +**Exit:** both states verified in the running app; `pnpm lint-fix` clean. **Phase 1 shippable.** + +--- + +# Phase 2 — Settings → Account toggle (follow-up) + +Additive. Nothing from Phase 1 changes except the body of `isNavSimplifiedAtom`. + +## Slice 5 — The override state + +Edit `web/oss/src/state/navPreference/atoms.ts`: + +```ts +import {atomWithStorage} from "jotai/utils" + +/** null = follow default, true = force simplified, false = force full. Per-user via LS. */ +export const simplifiedNavOverrideAtom = atomWithStorage( + "agenta:nav:simplified-override", + null, +) + +// Phase 2: explicit choice wins; else fall back to new-signup default. +export const isNavSimplifiedAtom = atom((get) => { + const override = get(simplifiedNavOverrideAtom) + return override ?? get(isNewUserAtom) +}) +``` + +Unit test: override `null` → `isNewUser`; override `true`/`false` → that value. + +**Exit:** derived atom follows the table in `context.md`, proven by the test. Sidebar behavior +from Phase 1 is unchanged when no override is set. + +## Slice 6 — The switch + +1. Add `web/oss/src/components/pages/settings/Account/NavigationPreference.tsx`: an antd `Switch` + labeled "Simplified navigation" with a one-line description ("Hide advanced features — + Prompts, Evaluations, Registry — for a focused agent workspace"). `checked` reads + `isNavSimplifiedAtom`; `onChange` writes the boolean to `simplifiedNavOverrideAtom`. +2. Render it in the Account tab, above `DeleteAccount`, at + `pages/w/[workspace_id]/p/[project_id]/settings/index.tsx:171` (`case "account"`). + +**Exit:** toggling shows/hides the advanced items live (no reload); the choice survives a +reload; both directions verified. Non-new users can now opt into simplified, and new users can +reveal everything. + +--- + +## Files touched + +**Phase 1** +- `web/oss/src/state/navPreference/atoms.ts` — new, `isNavSimplifiedAtom` (passthrough). +- `web/oss/src/components/Sidebar/hooks/useHideAdvancedNav.ts` — new, ~4 lines. +- `web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx` — five `isHidden` edits + + hook call + two dep-array entries. +- Sidebar visibility test (new or extended). + +**Phase 2** +- `web/oss/src/state/navPreference/atoms.ts` — add `simplifiedNavOverrideAtom`, extend the + derived atom. +- `web/oss/src/components/pages/settings/Account/NavigationPreference.tsx` — new switch. +- `web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx` — render the switch in + the `account` case. + +No changes to `engine/`, `scopes/`, `lib/onboarding/`, or any EE file, in either phase. + +## Rollback + +Phase 1: delete the two new files, the hook call, and the five `isHidden` edits — pure code +revert, no data migration, no server state. Phase 2: delete the switch + settings render and +revert the derived atom to the passthrough; the harmless `agenta:nav:simplified-override` +localStorage key can be left. diff --git a/docs/design/simplify-nav-new-users/research.md b/docs/design/simplify-nav-new-users/research.md new file mode 100644 index 0000000000..333cd0d6ed --- /dev/null +++ b/docs/design/simplify-nav-new-users/research.md @@ -0,0 +1,127 @@ +# Research — how the sidebar and new-user state work today + +All line references are current at the time of writing. + +## 1. One file owns every target item + +`web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx` builds two arrays: + +- `projectItems` (project scope) — `web/.../useSidebarConfig/index.tsx:52` +- `appItems` (app scope) — `web/.../useSidebarConfig/index.tsx:144` + +`projectItems` feeds the main sidebar via `mainScope.tsx:48`. `appItems` feeds the workflow +sidebar via `workflowItems.ts:29`. No EE file overrides `useSidebarConfig`, `mainScope`, or +`workflowItems` (verified with `find ee/src`), so a single OSS change reaches both editions. + +### Target items and their exact lines + +| Item | Scope | Key | Line | +| --- | --- | --- | --- | +| Prompts | project | `PROMPTS_SIDEBAR_KEY` | `:72` | +| Evaluation (whole group) | project | `evaluation-group` | `:88` | +| Overview | app | `overview-link` | `:147` | +| Registry | app | `app-variants-link` | `:164` | +| Evaluations | app | `app-evaluations-link` | `:174` | + +The Evaluation group at `:88` is a single item with a `submenu` (Test sets, Evaluators, +Evaluation runs, Annotation Queues). Hiding the group hides all four children. + +## 2. The `isHidden` mechanism already does exactly what we need + +`SidebarConfig.isHidden` (`engine/types.ts:12`) is honored by +`engine/visibility.ts:filterVisibleItems`: + +```ts +items.flatMap((item) => + item.isHidden ? [] : [{...item, submenu: item.submenu ? filterVisibleItems(item.submenu) : undefined}]) +``` + +It runs recursively and drops flagged items at every level, so a hidden item never renders, +never auto-opens, and never becomes the selected key. `filterVisibleSections` then drops any +section left empty. This is the sanctioned seam — set `isHidden` and the engine does the rest. + +**Seam detail:** `projectItems` targets (Prompts, evaluation-group) have no `isHidden` today, +so we add one. `appItems` targets already carry `isHidden` bound to the app-context gate +`const isHidden = !hasAppContext && !currentApp && !recentlyVisitedAppId` (`:145`). We must +**OR** our condition onto that existing value, not overwrite it, or we would re-show items +that the app-context gate intends to hide. + +## 3. The "new user + localStorage" infrastructure already exists — reuse it + +`web/oss/src/lib/onboarding/atoms.ts`: + +- `isNewUserAtom` (`:45`) reads/writes a per-user, localStorage-backed value under + `agenta:onboarding:${userId}:is-new-user` (`atomWithStorage`, `:22`). +- It returns `false` when `onboardingStorageUserIdAtom` (`:14`, LS key + `agenta:onboarding:active-user-id`) is unset. + +Lifecycle, verified by grep: + +- **Set `true`** on first signup in `web/oss/src/hooks/usePostAuthRedirect.ts:136` (EE) and + `:147` (OSS), driven by `is_new_user` in the SuperTokens session payload. +- **Set `false`**: nowhere. `grep` for `setIsNewUser(false)` / `isNewUser(false)` returns no + call. The atom's own doc comment claims it flips on tour completion, but that code path does + not exist. So today the flag is **sticky-true per browser** once a user signs up. +- `onboardingStorageUserIdAtom` is set for every authed user (`usePostAuthRedirect.ts:114`), + but `isNewUser` is only *written* when the payload says new. A returning user on a fresh + browser therefore reads the family default `false` → full nav. Only genuine fresh signups + read `true`. + +Already consumed the same way by observability onboarding +(`components/pages/observability/components/{ObservabilityTable,SessionsTable}/index.tsx`) and +the onboarding widget, so `useAtomValue(isNewUserAtom)` inside a hook is an established pattern. + +## 4. One interaction, not a current blocker + +`components/Onboarding/tours/firstEvaluationTour.ts:188` targets +`selector: '[data-tour="evaluations-nav"]'` — the app-scope Evaluations item we will hide. +But the whole tour engine is parked: `lib/onboarding/constants.ts:8` sets +`ONBOARDING_TOURS_ENABLED = false`, and the tour is registered/started only under that flag +(`Playground/PlaygroundOnboarding.tsx:23`). So there is no live conflict today. +`deployPromptTour.ts:33` references the Registry nav item but the selector is commented out. + +**Follow-up (not this delivery):** if tours are re-enabled, `firstEvaluationTour` must either +not run for new users or not depend on the hidden nav item. + +## 5. SSR / hydration note + +`atomWithStorage` hydrates from localStorage on the client. During SSR and first paint the +value is the default (`false`), so a new user could see the advanced items for one frame +before they disappear on hydration. This matches the existing observability-onboarding +behavior (same atom, same pattern), so it is acceptable for the MVP. If the flash is +objectionable, `atomWithStorage(..., {getOnInit: true})` or gating render on a +mounted/hydrated flag are the standard fixes — noted, not required. + +## 6. The settings surface for the toggle already exists + +Settings tabs are declared in +`web/oss/src/components/pages/settings/assets/navigation.ts`. There is already an `account` +tab (`SettingsTabKey`), and its content renders at +`web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx:171` — +`case "account": ... content: `. The Account content folder +(`components/pages/settings/Account/`) currently holds only `DeleteAccount.tsx`, so it is a +clean place to add a "Simplified navigation" switch above the delete-account block. + +## 7. Per-user vs per-workspace — why the toggle is the chosen shape + +The intent ("simplify for agent-focused people") is really a per-*workspace* property, but the +only ready signal (`isNewUser`) and the chosen storage (localStorage) are per-*user*. A per-user +preference therefore cannot force a whole team to match. Verified consequence in the invite flow: +`usePostAuthRedirect.ts` calls `setIsNewUser(true)` only in the **non-invited** branches +(`:136`, `:147`); an invited teammate returns at `:128–:131` before the flag is set, so they read +`isNewUser === false` → full nav, even when the inviter (a fresh agent signup) sees the simplified +nav. The toggle resolves this at the individual level: the teammate flips one switch to match. +Team-wide enforcement would require a workspace-level flag (backend), which is deferred. + +## Seams to pin in the plan + +1. A per-user preference atom `simplifiedNavOverrideAtom` (`boolean | null`, localStorage) plus + a derived `isNavSimplifiedAtom = override ?? isNewUser`. One value, two consumers. +2. `projectItems`: Prompts and evaluation-group need a new `isHidden` bound to + `isNavSimplifiedAtom`. +3. `appItems`: Overview, Registry, Evaluations need `isHidden` **OR-ed** with the existing + app-context `isHidden`, not replaced. +4. The condition must reach the sidebar through a single hook/selector so it lives in one place. +5. Non-targets (Home, Agents, Observability, Playground) must remain untouched. +6. The Settings → Account switch reads and writes the same preference, so the sidebar and the + switch never disagree. diff --git a/docs/design/simplify-nav-new-users/status.md b/docs/design/simplify-nav-new-users/status.md new file mode 100644 index 0000000000..d80e38af71 --- /dev/null +++ b/docs/design/simplify-nav-new-users/status.md @@ -0,0 +1,53 @@ +# Status — Simplify nav for new signup users + +**Last updated:** 2026-07-24 + +## Current stage + +Planning complete, split into two phases. **Phase 1** (ship now): hide the advanced nav for +new signups. **Phase 2** (follow-up): a per-user "Simplified navigation" toggle in +Settings → Account. No code written. Ready to implement Phase 1 on its own branch. + +## Locked decisions + +- **Phased delivery.** Phase 1 = hide-for-new-signups. Phase 2 = the Settings → Account toggle. + Phase 2 is additive — it touches no Phase-1 file except the body of `isNavSimplifiedAtom`. +- **Hide scope: nav-only.** Remove the sidebar entries only. No route guards, no in-app link + changes. +- **Items hidden.** Project: Prompts, Evaluation group (all four children). App: Overview, + Registry, Evaluations. +- **Items kept.** Home, Agents, project Observability; app Playground, app Observability. +- **Default mode = seeded by `isNewUser`.** New signups default to simplified; everyone else + defaults to full. No new signup-time storage — reuse `isNewUserAtom`. +- **Stable seam.** The sidebar hook `useHideAdvancedNav` reads one derived atom + `isNavSimplifiedAtom`. Phase 1: `= isNewUser`. Phase 2: `= override ?? isNewUser`. The hook + and the five sidebar edits are written once and never change. +- **Phase 1 has no escape hatch (accepted).** A genuinely-new solo user stays simplified until + Phase 2. Acceptable because that user is exactly the target audience and Phase 2 follows; + invited teammates already see the full nav (not flagged). +- **Phase 2 override.** A per-user localStorage `simplifiedNavOverrideAtom` + (`null | true | false`); switch in **Settings → Account**. Graduation becomes manual, anytime. +- **No backend, no team-wide enforcement.** Per-user preference. An invited teammate flips the + switch (Phase 2) to match the team; we do not force a whole workspace to one mode. A + workspace-level flag (backend) would, and is deferred; the override model does not block it. +- **Mechanism: the existing `isHidden` flag** + `filterVisibleItems` engine. No engine change. +- **App-scope OR-ing.** Overview/Registry/Evaluations: OR the condition onto the existing + app-context `isHidden`, do not replace it. +- **One OSS sidebar file** changes; both editions inherit it (no EE fork exists). +- **Parked tour: no action.** Tours stay disabled (`ONBOARDING_TOURS_ENABLED = false`), so + `firstEvaluationTour`'s reference to the hidden Evaluations item is a non-issue. +- **Empty sections: filter them out.** `filterVisibleSections` already drops zero-item + sections; QA confirms nothing residual renders. +- **Override scope (Phase 2): global-per-browser.** One `agenta:nav:simplified-override` key, + not per-user. Accepted the shared-browser leak as negligible for MVP. +- **SSR/hydration flash: accept and judge in QA.** Ship as-is; only reach for `getOnInit` / + a hydrated-gate if the one-frame flash actually looks bad. + +## Open implementation questions + +None. All resolved (2026-07-24). Ready to build. + +## Next action + +Implement Phase 1, Slice 0 (pin current sidebar with a test) on a fresh branch, then +Slices 1–4. Phase 2 (Slices 5–6) follows as a separate delivery. From 367b59aa9b47a805fa8f614f6e6f54e192f3472e Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 24 Jul 2026 18:12:44 +0600 Subject: [PATCH 2/7] feat(onboarding): add navSimplifiedDefault atom and use itIntroduce a 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. --- docs/design/simplify-nav-new-users/README.md | 8 +- docs/design/simplify-nav-new-users/context.md | 23 +-- docs/design/simplify-nav-new-users/plan.md | 131 +++++++++++------- .../design/simplify-nav-new-users/research.md | 17 ++- docs/design/simplify-nav-new-users/status.md | 35 +++-- .../Sidebar/hooks/useSidebarConfig/index.tsx | 18 ++- web/oss/src/hooks/usePostAuthRedirect.ts | 9 +- web/oss/src/lib/onboarding/atoms.ts | 26 ++++ web/oss/src/lib/onboarding/index.ts | 1 + web/oss/src/state/onboarding/selectors.ts | 14 +- 10 files changed, 198 insertions(+), 84 deletions(-) diff --git a/docs/design/simplify-nav-new-users/README.md b/docs/design/simplify-nav-new-users/README.md index feefa21b66..6584323f07 100644 --- a/docs/design/simplify-nav-new-users/README.md +++ b/docs/design/simplify-nav-new-users/README.md @@ -17,9 +17,11 @@ returning users default to the full view throughout. - Nav-only — hide sidebar entries; no route guards, no in-app link changes. - Hide when simplified: Prompts, Evaluation group (project); Overview, Registry, Evaluations (app). - Keep always: Home, Agents, Observability, app Playground. -- Default mode seeded by `isNewUser` (new signups → simplified); everyone else → full. -- Stable seam: the sidebar hook reads `isNavSimplifiedAtom` — Phase 1 `= isNewUser`, Phase 2 - `= override ?? isNewUser`. +- Default mode seeded by a fresh forward-only key `navSimplifiedDefaultAtom`, written at signup + (new signups → simplified); everyone else → full. Deliberately **not** `isNewUserAtom`, which + is sticky-true for existing users and would strip their advanced nav. +- Stable seam: the sidebar reads one derived atom `advancedNavHiddenAtom` (in `state/onboarding` + selectors) — Phase 1 `= navSimplifiedDefault`, Phase 2 `= override ?? navSimplifiedDefault`. - No backend, no team-wide enforcement — per-user preference; an invited teammate flips the Phase-2 switch to match their team. A workspace-level flag is deferred, not blocked. - Built on the sidebar's existing `isHidden` mechanism; one OSS sidebar file, both editions inherit it. diff --git a/docs/design/simplify-nav-new-users/context.md b/docs/design/simplify-nav-new-users/context.md index a4fdf0c9e9..a9384ece59 100644 --- a/docs/design/simplify-nav-new-users/context.md +++ b/docs/design/simplify-nav-new-users/context.md @@ -33,10 +33,13 @@ Observability. ### Phase 1 (this delivery) — hide for new signups -New signup users (`isNewUser === true`) get the simplified sidebar. Everyone else is -unaffected. There is no switch yet, so a genuinely-new solo user stays simplified until -Phase 2 ships. Invited teammates already read `isNewUser === false` (see research.md §7), so -they keep the full nav. +New signup users get the simplified sidebar, flagged by a fresh forward-only key +(`navSimplifiedDefaultAtom`, `agenta:onboarding::nav-simplified`) seeded at signup. +Everyone else is unaffected — existing users never have the key, so they keep the full nav. We +do not reuse `isNewUserAtom` (sticky-true for everyone who ever signed up; reusing it would +break current users). There is no switch yet, so a genuinely-new solo user stays simplified +until Phase 2 ships. Invited teammates are never flagged (see research.md §7), so they keep the +full nav. ### Phase 2 (follow-up) — the toggle @@ -59,14 +62,16 @@ is additive — it changes no Phase-1 file except the one derived atom. ## Product language -- **New signup user** — a user for whom `isNewUserAtom` is `true`. Set once on first signup, - persisted per-user in localStorage, never cleared today. +- **New signup user** — a user who signs up after this ships, flagged by the fresh + `navSimplifiedDefaultAtom` key (`agenta:onboarding::nav-simplified`), seeded `true` at + signup alongside `setIsNewUser(true)`. Distinct from `isNewUserAtom`, which is sticky-true for + existing users and is deliberately not reused. - **Simplified navigation** — the reduced, agent-focused sidebar (advanced items hidden). - **Full navigation** — the complete sidebar (today's behavior). -- **Simplified-nav preference** — the per-user override (`simplifiedNavOverrideAtom`): +- **Simplified-nav preference** — the per-user override (`simplifiedNavOverrideAtom`, Phase 2): `null` = follow the default, `true` = force simplified, `false` = force full. -- **Effective mode** — `override ?? isNewUser`, exposed as `isNavSimplifiedAtom`. This single - value drives both the sidebar and the settings switch. +- **Effective mode** — `override ?? navSimplifiedDefault`, exposed as `advancedNavHiddenAtom` + (in `state/onboarding` selectors). This single value drives both the sidebar and the switch. ## Success criteria diff --git a/docs/design/simplify-nav-new-users/plan.md b/docs/design/simplify-nav-new-users/plan.md index a15e809971..c453d3fd6b 100644 --- a/docs/design/simplify-nav-new-users/plan.md +++ b/docs/design/simplify-nav-new-users/plan.md @@ -8,23 +8,31 @@ one derived atom. ## Scope - Nav-only. No route guards, no in-app link changes. No backend. No team-wide enforcement. -- **Phase 1:** new signups (`isNewUser`) get the simplified sidebar; everyone else is - unaffected. No way to switch yet — a genuinely-new solo user stays simplified until Phase 2. - (Invited teammates already read `isNewUser === false`, so they keep the full nav — see - research.md §7.) +- **Phase 1:** new signups (flagged by the fresh `nav-simplified` key) get the simplified + sidebar; everyone else — including all existing users — is unaffected. No way to switch yet, so + a genuinely-new solo user stays simplified until Phase 2. (Invited teammates are never flagged, + so they keep the full nav — see research.md §7.) - **Phase 2:** a per-user localStorage override + a switch in Settings → Account, so anyone can force simplified or full. ## The stable seam -One derived atom is the sole dependency of the sidebar hook, so the sidebar edits are written -once (Phase 1) and never touched again: +One derived atom is the sole nav-simplify dependency the sidebar reads, so the sidebar edits are +written once (Phase 1) and never touched again. It lives in `state/onboarding/selectors.ts` +alongside the other onboarding-driven nav gates (`deadEndNavDisabledAtom`, `homeNavInertAtom`): -- **Phase 1:** `isNavSimplifiedAtom = isNewUser` (thin passthrough). -- **Phase 2:** `isNavSimplifiedAtom = override ?? isNewUser` (add the override; same name, same - consumers). +- **Phase 1:** `advancedNavHiddenAtom = navSimplifiedDefault` (thin passthrough over a fresh, + forward-only per-user key). +- **Phase 2:** `advancedNavHiddenAtom = override ?? navSimplifiedDefault` (add the override; same + name, same consumers). -The hook `useHideAdvancedNav` reads `isNavSimplifiedAtom` in both phases and never changes. +**Why not reuse `isNewUserAtom`:** it is sticky-true for everyone who ever signed up (including +existing users), so deriving from it would strip the advanced nav from current users. Phase 1 +instead introduces a new per-user key `agenta:onboarding::nav-simplified` +(`navSimplifiedDefaultAtom`, default `false`) written only on signups going forward. + +The sidebar reads `advancedNavHiddenAtom` with a bare `useAtomValue` (matching its sibling nav +gates); only the atom's body changes between phases. --- @@ -38,41 +46,60 @@ The hook `useHideAdvancedNav` reads `isNavSimplifiedAtom` in both phases and nev **Exit:** a passing test capturing the pre-change sidebar (targets + non-targets visible). -## Slice 1 — The derived atom + hook +## Slice 1 — The forward-only default + the derived seam atom -1. Add `web/oss/src/state/navPreference/atoms.ts`: +1. Add the durable per-user default in `web/oss/src/lib/onboarding/atoms.ts` (reuses the + existing per-user scoping infra — `onboardingStorageUserIdAtom`, `createScopedStorageKey`): ```ts - import {isNewUserAtom} from "@/oss/lib/onboarding" - import {atom} from "jotai" - - // Phase 1: simplified nav follows new-signup status. Phase 2 adds a user override here. - export const isNavSimplifiedAtom = atom((get) => get(isNewUserAtom)) + const navSimplifiedDefaultAtomFamily = atomFamily((userId: string) => + atomWithStorage(createScopedStorageKey(userId, "nav-simplified"), false), + ) + + export const navSimplifiedDefaultAtom = atom( + (get) => { + const userId = get(onboardingStorageUserIdAtom) + return userId ? get(navSimplifiedDefaultAtomFamily(userId)) : false + }, + (get, set, next: boolean) => { + const userId = get(onboardingStorageUserIdAtom) + if (userId) set(navSimplifiedDefaultAtomFamily(userId), next) + }, + ) ``` -2. Add `web/oss/src/components/Sidebar/hooks/useHideAdvancedNav.ts`: + Export it from the `lib/onboarding` barrel. - ```ts - import {useAtomValue} from "jotai" - import {isNavSimplifiedAtom} from "@/oss/state/navPreference/atoms" +2. Seed it at signup in `web/oss/src/hooks/usePostAuthRedirect.ts`: call + `setNavSimplifiedDefault(true)` next to each `setIsNewUser(true)` (EE and OSS non-invited + branches). Invited users `return` before this, so they stay on full nav. - /** Advanced sidebar areas are hidden while the simplified view is active. */ - export const useHideAdvancedNav = (): boolean => useAtomValue(isNavSimplifiedAtom) +3. Add the derived seam atom to `web/oss/src/state/onboarding/selectors.ts`, next to + `deadEndNavDisabledAtom` (imports `navSimplifiedDefaultAtom` from `@/oss/lib/onboarding/atoms`): + + ```ts + // Phase 1: follows the signup-era default. Phase 2 adds a user override here. + export const advancedNavHiddenAtom = atom((get) => get(navSimplifiedDefaultAtom)) ``` -**Exit:** `isNavSimplifiedAtom` returns `isNewUser`; hook compiles and is unit-referenced. + No new module or wrapper hook — consumers read it with a bare `useAtomValue`, like the + sibling nav gates. + +**Exit:** `advancedNavHiddenAtom` returns the new per-user default (not `isNewUser`); a signup +sets `nav-simplified` true; both editions compile. ## Slice 2 — Hide the two project-scope items Edit `web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx`. -1. `const hideAdvancedNav = useHideAdvancedNav()` near the top. +1. `const hideAdvancedNav = useAtomValue(advancedNavHiddenAtom)` near the top (import the atom + from the existing `@/oss/state/onboarding` barrel). 2. Prompts (`PROMPTS_SIDEBAR_KEY`, `:72`): add `isHidden: hideAdvancedNav`. 3. Evaluation group (`evaluation-group`, `:88`): add `isHidden: hideAdvancedNav`. 4. Add `hideAdvancedNav` to the `projectItems` `useMemo` deps. -**Exit:** with `isNewUser` true, `projectItems` (post `filterVisibleItems`) has no Prompts and -no `evaluation-group`; with it false, both present. Non-targets unchanged. +**Exit:** with the simplified default true, `projectItems` (post `filterVisibleItems`) has no +Prompts and no `evaluation-group`; with it false, both present. Non-targets unchanged. ## Slice 3 — Hide the three app-scope items @@ -82,17 +109,17 @@ Same file, `appItems` memo (`:144`). `isHidden: isHidden || hideAdvancedNav`. Do **not** touch Playground or app Observability. 2. Add `hideAdvancedNav` to the `appItems` `useMemo` deps. -**Exit:** with `isNewUser` true, `appItems` (post `filterVisibleItems`) has no Overview, -Registry, or Evaluations, and still has Playground + Observability when the app-context gate -allows. With it false, all five behave exactly as before. +**Exit:** with the simplified default true, `appItems` (post `filterVisibleItems`) has no +Overview, Registry, or Evaluations, and still has Playground + Observability when the app-context +gate allows. With it false, all five behave exactly as before. ## Slice 4 — Manual QA (Phase 1) 1. Run the local stack (OSS + dev per root `AGENTS.md`). 2. New user: set `agenta:onboarding:active-user-id` to the user id and - `agenta:onboarding::is-new-user` to `true`, reload → simplified sidebar. Confirm the + `agenta:onboarding::nav-simplified` to `true`, reload → simplified sidebar. Confirm the five items gone, Home/Agents/Observability/Playground remain. -3. Set `is-new-user` to `false`, reload → full sidebar returns. +3. Set `nav-simplified` to `false` (or remove the key), reload → full sidebar returns. 4. Check both the main (project) and workflow (app) sidebars. 5. Confirm no empty section header / stray divider where the Evaluation group was (`filterVisibleSections` drops empty sections — verify visually). @@ -103,11 +130,12 @@ allows. With it false, all five behave exactly as before. # Phase 2 — Settings → Account toggle (follow-up) -Additive. Nothing from Phase 1 changes except the body of `isNavSimplifiedAtom`. +Additive. Nothing from Phase 1 changes except the body of `advancedNavHiddenAtom`. ## Slice 5 — The override state -Edit `web/oss/src/state/navPreference/atoms.ts`: +Add the override atom (near `navSimplifiedDefaultAtom`) and extend `advancedNavHiddenAtom` in +`web/oss/src/state/onboarding/selectors.ts`: ```ts import {atomWithStorage} from "jotai/utils" @@ -118,14 +146,14 @@ export const simplifiedNavOverrideAtom = atomWithStorage( null, ) -// Phase 2: explicit choice wins; else fall back to new-signup default. -export const isNavSimplifiedAtom = atom((get) => { +// Phase 2: explicit choice wins; else fall back to the signup-era default. +export const advancedNavHiddenAtom = atom((get) => { const override = get(simplifiedNavOverrideAtom) - return override ?? get(isNewUserAtom) + return override ?? get(navSimplifiedDefaultAtom) }) ``` -Unit test: override `null` → `isNewUser`; override `true`/`false` → that value. +Unit test: override `null` → `navSimplifiedDefault`; override `true`/`false` → that value. **Exit:** derived atom follows the table in `context.md`, proven by the test. Sidebar behavior from Phase 1 is unchanged when no override is set. @@ -135,7 +163,7 @@ from Phase 1 is unchanged when no override is set. 1. Add `web/oss/src/components/pages/settings/Account/NavigationPreference.tsx`: an antd `Switch` labeled "Simplified navigation" with a one-line description ("Hide advanced features — Prompts, Evaluations, Registry — for a focused agent workspace"). `checked` reads - `isNavSimplifiedAtom`; `onChange` writes the boolean to `simplifiedNavOverrideAtom`. + `advancedNavHiddenAtom`; `onChange` writes the boolean to `simplifiedNavOverrideAtom`. 2. Render it in the Account tab, above `DeleteAccount`, at `pages/w/[workspace_id]/p/[project_id]/settings/index.tsx:171` (`case "account"`). @@ -148,24 +176,27 @@ reveal everything. ## Files touched **Phase 1** -- `web/oss/src/state/navPreference/atoms.ts` — new, `isNavSimplifiedAtom` (passthrough). -- `web/oss/src/components/Sidebar/hooks/useHideAdvancedNav.ts` — new, ~4 lines. +- `web/oss/src/lib/onboarding/atoms.ts` — new `navSimplifiedDefaultAtom` + family + storage key. +- `web/oss/src/lib/onboarding/index.ts` — export `navSimplifiedDefaultAtom` from the barrel. +- `web/oss/src/hooks/usePostAuthRedirect.ts` — seed the default at signup (two call sites). +- `web/oss/src/state/onboarding/selectors.ts` — new `advancedNavHiddenAtom` (passthrough seam), + next to `deadEndNavDisabledAtom`. - `web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx` — five `isHidden` edits + - hook call + two dep-array entries. -- Sidebar visibility test (new or extended). + `useAtomValue(advancedNavHiddenAtom)` + two dep-array entries. **Phase 2** -- `web/oss/src/state/navPreference/atoms.ts` — add `simplifiedNavOverrideAtom`, extend the - derived atom. +- `web/oss/src/state/onboarding/selectors.ts` — add `simplifiedNavOverrideAtom`, extend + `advancedNavHiddenAtom`. - `web/oss/src/components/pages/settings/Account/NavigationPreference.tsx` — new switch. - `web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx` — render the switch in the `account` case. -No changes to `engine/`, `scopes/`, `lib/onboarding/`, or any EE file, in either phase. +No changes to `engine/` or `scopes/`, or any EE file, in either phase. ## Rollback -Phase 1: delete the two new files, the hook call, and the five `isHidden` edits — pure code -revert, no data migration, no server state. Phase 2: delete the switch + settings render and -revert the derived atom to the passthrough; the harmless `agenta:nav:simplified-override` -localStorage key can be left. +Phase 1: revert `advancedNavHiddenAtom`, `navSimplifiedDefaultAtom` + signup seed, the sidebar +`useAtomValue` read, and the five `isHidden` edits — pure code revert, no data migration, no +server state. The harmless `agenta:onboarding::nav-simplified` localStorage key can be +left. Phase 2: delete the switch + settings render and revert `advancedNavHiddenAtom` to the +passthrough; the harmless `agenta:nav:simplified-override` localStorage key can be left. diff --git a/docs/design/simplify-nav-new-users/research.md b/docs/design/simplify-nav-new-users/research.md index 333cd0d6ed..3b6b89b011 100644 --- a/docs/design/simplify-nav-new-users/research.md +++ b/docs/design/simplify-nav-new-users/research.md @@ -71,6 +71,13 @@ Already consumed the same way by observability onboarding (`components/pages/observability/components/{ObservabilityTable,SessionsTable}/index.tsx`) and the onboarding widget, so `useAtomValue(isNewUserAtom)` inside a hook is an established pattern. +**Decision (do not reuse this atom):** because `isNewUser` is sticky-true for everyone who has +ever signed up (including existing users), deriving the sidebar from it would strip advanced nav +from current users. Phase 1 instead adds a **fresh** per-user key +`agenta:onboarding::nav-simplified` (`navSimplifiedDefaultAtom`), written only on signups +going forward — seeded next to `setIsNewUser(true)` in `usePostAuthRedirect`. It shares this +file's per-user scoping infra but is a distinct value. + ## 4. One interaction, not a current blocker `components/Onboarding/tours/firstEvaluationTour.ts:188` targets @@ -115,13 +122,15 @@ Team-wide enforcement would require a workspace-level flag (backend), which is d ## Seams to pin in the plan -1. A per-user preference atom `simplifiedNavOverrideAtom` (`boolean | null`, localStorage) plus - a derived `isNavSimplifiedAtom = override ?? isNewUser`. One value, two consumers. +1. A per-user preference atom `simplifiedNavOverrideAtom` (`boolean | null`, localStorage, Phase + 2) plus a derived `advancedNavHiddenAtom = override ?? navSimplifiedDefault`, homed in + `state/onboarding/selectors.ts` next to `deadEndNavDisabledAtom`. The default is a **fresh** + forward-only key (`nav-simplified`), NOT `isNewUserAtom` — see §3. One value, two consumers. 2. `projectItems`: Prompts and evaluation-group need a new `isHidden` bound to - `isNavSimplifiedAtom`. + `advancedNavHiddenAtom`. 3. `appItems`: Overview, Registry, Evaluations need `isHidden` **OR-ed** with the existing app-context `isHidden`, not replaced. -4. The condition must reach the sidebar through a single hook/selector so it lives in one place. +4. The condition must reach the sidebar through a single selector so it lives in one place. 5. Non-targets (Home, Agents, Observability, Playground) must remain untouched. 6. The Settings → Account switch reads and writes the same preference, so the sidebar and the switch never disagree. diff --git a/docs/design/simplify-nav-new-users/status.md b/docs/design/simplify-nav-new-users/status.md index d80e38af71..bb35252a99 100644 --- a/docs/design/simplify-nav-new-users/status.md +++ b/docs/design/simplify-nav-new-users/status.md @@ -4,24 +4,31 @@ ## Current stage -Planning complete, split into two phases. **Phase 1** (ship now): hide the advanced nav for -new signups. **Phase 2** (follow-up): a per-user "Simplified navigation" toggle in -Settings → Account. No code written. Ready to implement Phase 1 on its own branch. +Split into two phases. **Phase 1** (hide the advanced nav for new signups) is **implemented** — +`advancedNavHiddenAtom` added to `state/onboarding` selectors, five `isHidden` edits in +`useSidebarConfig`; `tsc` and ESLint pass clean. Remaining Phase-1 step: manual QA (Slice 4). +**Phase 2** (a per-user "Simplified navigation" toggle in Settings → Account) is not started. ## Locked decisions - **Phased delivery.** Phase 1 = hide-for-new-signups. Phase 2 = the Settings → Account toggle. - Phase 2 is additive — it touches no Phase-1 file except the body of `isNavSimplifiedAtom`. + Phase 2 is additive — it touches no Phase-1 file except the body of `advancedNavHiddenAtom`. - **Hide scope: nav-only.** Remove the sidebar entries only. No route guards, no in-app link changes. - **Items hidden.** Project: Prompts, Evaluation group (all four children). App: Overview, Registry, Evaluations. - **Items kept.** Home, Agents, project Observability; app Playground, app Observability. -- **Default mode = seeded by `isNewUser`.** New signups default to simplified; everyone else - defaults to full. No new signup-time storage — reuse `isNewUserAtom`. -- **Stable seam.** The sidebar hook `useHideAdvancedNav` reads one derived atom - `isNavSimplifiedAtom`. Phase 1: `= isNewUser`. Phase 2: `= override ?? isNewUser`. The hook - and the five sidebar edits are written once and never change. +- **Default mode = a fresh forward-only key, seeded at signup.** New signups default to + simplified; everyone else defaults to full. We do **not** reuse `isNewUserAtom`: it is + sticky-true for everyone who ever signed up (incl. existing users), so deriving from it would + strip advanced nav from current users. Instead a new per-user key + `agenta:onboarding::nav-simplified` (`navSimplifiedDefaultAtom`, default `false`) is + written only on signups going forward, alongside `setIsNewUser(true)` in `usePostAuthRedirect`. + Existing users never have the key → full nav, unchanged. +- **Stable seam.** The sidebar reads one derived atom `advancedNavHiddenAtom` (in the + `state/onboarding` selectors module, alongside `deadEndNavDisabledAtom`). Phase 1: + `= navSimplifiedDefault`. Phase 2: `= override ?? navSimplifiedDefault`. Only this atom's body + and the five sidebar edits change; consumers never do. - **Phase 1 has no escape hatch (accepted).** A genuinely-new solo user stays simplified until Phase 2. Acceptable because that user is exactly the target audience and Phase 2 follows; invited teammates already see the full nav (not flagged). @@ -49,5 +56,11 @@ None. All resolved (2026-07-24). Ready to build. ## Next action -Implement Phase 1, Slice 0 (pin current sidebar with a test) on a fresh branch, then -Slices 1–4. Phase 2 (Slices 5–6) follows as a separate delivery. +Manual QA Phase 1 in the running app (Slice 4): flip `agenta:onboarding::nav-simplified` +true/false and confirm the five items hide/show, non-targets stay, no empty section header +remains. Then Phase 2 (Slices 5–6) as a separate delivery. + +**Slice 0 note:** the automated pin was skipped — `@agenta/oss` has no CI-wired vitest runner +(the web unit layer only runs `test:unit` across `@agenta/*` packages), so a test in `oss/src` +would never execute. Covered instead by `tsc` + ESLint + manual QA. Standing up an oss vitest +harness is a possible follow-up if automated sidebar coverage is wanted. diff --git a/web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx b/web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx index 3cc0f3cf1b..3f08fc578a 100644 --- a/web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx +++ b/web/oss/src/components/Sidebar/hooks/useSidebarConfig/index.tsx @@ -18,7 +18,11 @@ import {getEntityKindIcon} from "@/oss/components/References" import useURL from "@/oss/hooks/useURL" import {useCurrentAppLite} from "@/oss/state/app" import {useAppState} from "@/oss/state/appState" -import {deadEndNavDisabledAtom, homeNavInertAtom} from "@/oss/state/onboarding" +import { + advancedNavHiddenAtom, + deadEndNavDisabledAtom, + homeNavInertAtom, +} from "@/oss/state/onboarding" import { AGENTS_SIDEBAR_KEY, @@ -45,6 +49,7 @@ export const useSidebarConfig = (): MainSidebarItems => { const dynamicChildren = useSidebarDynamicChildren() const homeNavInert = useAtomValue(homeNavInertAtom) const deadEndNavDisabled = useAtomValue(deadEndNavDisabledAtom) + const hideAdvancedNav = useAtomValue(advancedNavHiddenAtom) const hasProjectURL = Boolean(projectURL) const hasAppContext = routeLayer === "app" && Boolean(routedAppId || appURL || recentlyVisitedAppURL) @@ -74,6 +79,7 @@ export const useSidebarConfig = (): MainSidebarItems => { title: "Prompts", link: `${projectURL}/prompts`, icon: getEntityKindIcon("app"), + isHidden: hideAdvancedNav, disabled: !hasProjectURL, }, { @@ -89,6 +95,7 @@ export const useSidebarConfig = (): MainSidebarItems => { key: "evaluation-group", title: "Evaluation", icon: , + isHidden: hideAdvancedNav, disabled: !hasProjectURL, submenu: [ { @@ -138,7 +145,7 @@ export const useSidebarConfig = (): MainSidebarItems => { disabled: !hasProjectURL, }, ], - [baseAppURL, deadEndNavDisabled, hasProjectURL, homeNavInert, projectURL], + [baseAppURL, deadEndNavDisabled, hasProjectURL, hideAdvancedNav, homeNavInert, projectURL], ) const appItems = useMemo(() => { @@ -149,7 +156,7 @@ export const useSidebarConfig = (): MainSidebarItems => { title: "Overview", link: `${appURL || recentlyVisitedAppURL}/overview`, icon: , - isHidden, + isHidden: isHidden || hideAdvancedNav, // Enabled for evaluators too — scoped by the workflow id as the `application` reference. disabled: !hasProjectURL, }, @@ -165,7 +172,7 @@ export const useSidebarConfig = (): MainSidebarItems => { key: "app-variants-link", title: "Registry", link: `${appURL || recentlyVisitedAppURL}/variants`, - isHidden, + isHidden: isHidden || hideAdvancedNav, icon: , disabled: !hasProjectURL, dataTour: "registry-nav", @@ -175,7 +182,7 @@ export const useSidebarConfig = (): MainSidebarItems => { key: "app-evaluations-link", title: "Evaluations", link: `${appURL || recentlyVisitedAppURL}/evaluations`, - isHidden, + isHidden: isHidden || hideAdvancedNav, icon: , // Enabled for evaluators too — shows the runs scoped by the evaluator's id. disabled: !hasProjectURL, @@ -195,6 +202,7 @@ export const useSidebarConfig = (): MainSidebarItems => { currentApp, hasAppContext, hasProjectURL, + hideAdvancedNav, recentlyVisitedAppId, recentlyVisitedAppURL, ]) diff --git a/web/oss/src/hooks/usePostAuthRedirect.ts b/web/oss/src/hooks/usePostAuthRedirect.ts index a52f489f29..13c1892388 100644 --- a/web/oss/src/hooks/usePostAuthRedirect.ts +++ b/web/oss/src/hooks/usePostAuthRedirect.ts @@ -9,7 +9,11 @@ import {writeLastAuthMethod} from "@/oss/components/pages/auth/assets/lastAuthMe import {queryClient} from "@/oss/lib/api/queryClient" import {filterOrgsByAuthMethod} from "@/oss/lib/helpers/authMethodFilter" import {isEE} from "@/oss/lib/helpers/isEE" -import {isNewUserAtom, onboardingStorageUserIdAtom} from "@/oss/lib/onboarding/atoms" +import { + isNewUserAtom, + navSimplifiedDefaultAtom, + onboardingStorageUserIdAtom, +} from "@/oss/lib/onboarding/atoms" import {mergeSessionIdentities} from "@/oss/services/auth/api" import {fetchAllOrgsList} from "@/oss/services/organization/api" import {orgsAtom, useOrgData} from "@/oss/state/org" @@ -54,6 +58,7 @@ const usePostAuthRedirect = () => { const authUpgradeOrgKey = "authUpgradeOrgId" const lastSsoOrgSlugKey = "lastSsoOrgSlug" const setIsNewUser = useSetAtom(isNewUserAtom) + const setNavSimplifiedDefault = useSetAtom(navSimplifiedDefaultAtom) const setOnboardingStorageUserId = useSetAtom(onboardingStorageUserIdAtom) const hasInviteFromQuery = useMemo(() => { @@ -134,6 +139,7 @@ const usePostAuthRedirect = () => { writePostSignupPending() await resetAuthState() setIsNewUser(true) + setNavSimplifiedDefault(true) await router.push("/post-signup") return } else { @@ -145,6 +151,7 @@ const usePostAuthRedirect = () => { ) await resetAuthState() setIsNewUser(true) + setNavSimplifiedDefault(true) } } diff --git a/web/oss/src/lib/onboarding/atoms.ts b/web/oss/src/lib/onboarding/atoms.ts index e339a0b4a8..8bf3c1f6ee 100644 --- a/web/oss/src/lib/onboarding/atoms.ts +++ b/web/oss/src/lib/onboarding/atoms.ts @@ -8,6 +8,7 @@ import type {CurrentStepState} from "./types" const STORAGE_KEYS = { ACTIVE_USER_ID: "agenta:onboarding:active-user-id", IS_NEW_USER: "is-new-user", + NAV_SIMPLIFIED: "nav-simplified", SEEN_TOURS: "seen-tours", } as const @@ -23,6 +24,10 @@ const isNewUserAtomFamily = atomFamily((userId: string) => atomWithStorage(createScopedStorageKey(userId, STORAGE_KEYS.IS_NEW_USER), false), ) +const navSimplifiedDefaultAtomFamily = atomFamily((userId: string) => + atomWithStorage(createScopedStorageKey(userId, STORAGE_KEYS.NAV_SIMPLIFIED), false), +) + const seenToursAtomFamily = atomFamily((userId: string) => atomWithStorage>( createScopedStorageKey(userId, STORAGE_KEYS.SEEN_TOURS), @@ -55,6 +60,27 @@ export const isNewUserAtom = atom( }, ) +/** + * Durable "this user signed up under the simplified-nav experience" flag. + * + * Distinct from {@link isNewUserAtom} on purpose: that flag is sticky-true for everyone + * who ever signed up, so reusing it would strip advanced nav from existing users. This one + * uses a fresh key that is only written on signups going forward — existing users default + * to `false` (full nav). Drives `advancedNavHiddenAtom` (state/onboarding selectors). + */ +export const navSimplifiedDefaultAtom = atom( + (get) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return false + return get(navSimplifiedDefaultAtomFamily(userId)) + }, + (get, set, next: boolean) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return + set(navSimplifiedDefaultAtomFamily(userId), next) + }, +) + /** * Tracks which tours have been seen/completed * diff --git a/web/oss/src/lib/onboarding/index.ts b/web/oss/src/lib/onboarding/index.ts index 4aaf955803..3b9f04a19d 100644 --- a/web/oss/src/lib/onboarding/index.ts +++ b/web/oss/src/lib/onboarding/index.ts @@ -42,6 +42,7 @@ export {defaultWidgetConfig} from "./widget" // Atoms export { isNewUserAtom, + navSimplifiedDefaultAtom, seenToursAtom, markTourSeenAtom, hasTourBeenSeenAtom, diff --git a/web/oss/src/state/onboarding/selectors.ts b/web/oss/src/state/onboarding/selectors.ts index 80480464df..6deb262eff 100644 --- a/web/oss/src/state/onboarding/selectors.ts +++ b/web/oss/src/state/onboarding/selectors.ts @@ -1,10 +1,13 @@ import {atom} from "jotai" +import {navSimplifiedDefaultAtom} from "@/oss/lib/onboarding/atoms" + import {onboardingSessionAtom} from "./atoms" /** * Pre-commit onboarding is the current surface (minting or describing an agent, no real agent - * yet). The one truth every app-behavior adjustment below derives from. + * yet). The truth the session-driven adjustments below derive from (the signup-era nav default + * that drives {@link advancedNavHiddenAtom} is separate). */ export const isOnboardingActiveAtom = atom((get) => { const {active, committedRevisionId} = get(onboardingSessionAtom) @@ -26,3 +29,12 @@ export const homeNavInertAtom = atom((get) => get(isOnboardingActiveAtom)) * disabled. Links that work app-less (Observability, Test sets, Evaluators, Prompts) stay live. */ export const deadEndNavDisabledAtom = atom((get) => get(isOnboardingActiveAtom)) + +/** + * The simplified, agent-focused sidebar hides advanced areas (Prompts, Evaluation, Registry, + * Evaluations, Overview). Unlike the selectors above, this derives from the durable signup-era + * default ({@link navSimplifiedDefaultAtom}) — set once at signup, not the live session — so + * existing users keep the full nav. Stable seam: Phase 2 layers a user override here + * (`override ?? default`) without touching consumers. + */ +export const advancedNavHiddenAtom = atom((get) => get(navSimplifiedDefaultAtom)) From 96f8a205e64bdbbc9c4cc7b8b14327dc99f8d430 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Fri, 24 Jul 2026 18:30:39 +0600 Subject: [PATCH 3/7] fix: workflow default navigation to playground --- .../components/Sidebar/dynamic/registry.ts | 4 ++-- .../components/pages/agents/AgentsPage.tsx | 5 +++-- .../components/pages/prompts/PromptsPage.tsx | 5 ++++- .../p/[project_id]/apps/[app_id]/index.tsx | 19 ++++++++----------- web/oss/src/state/app/atoms/fetcher.ts | 2 +- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/web/oss/src/components/Sidebar/dynamic/registry.ts b/web/oss/src/components/Sidebar/dynamic/registry.ts index d074b60d2b..489d43c7c7 100644 --- a/web/oss/src/components/Sidebar/dynamic/registry.ts +++ b/web/oss/src/components/Sidebar/dynamic/registry.ts @@ -62,7 +62,7 @@ const ENTITIES: SidebarEntity[] = [ kind: "app", listAtom: promptWorkflowsListQueryStateAtom, getLabel: (workflow) => workflow.name || workflow.slug || "Untitled prompt", - childPath: (workflow) => `/apps/${workflow.id}/overview`, + childPath: (workflow) => `/apps/${workflow.id}/playground`, emptyLabel: "No prompts", showAllPath: "/prompts", }), @@ -71,7 +71,7 @@ const ENTITIES: SidebarEntity[] = [ icon: createElement(RobotIcon, {size: 14}), listAtom: agentWorkflowsListQueryStateAtom, getLabel: (workflow) => workflow.name || workflow.slug || "Untitled agent", - childPath: (workflow) => `/apps/${workflow.id}/overview`, + childPath: (workflow) => `/apps/${workflow.id}/playground`, emptyLabel: "No agents", showAllPath: "/agents", }), diff --git a/web/oss/src/components/pages/agents/AgentsPage.tsx b/web/oss/src/components/pages/agents/AgentsPage.tsx index 214fb22e39..84175f7c81 100644 --- a/web/oss/src/components/pages/agents/AgentsPage.tsx +++ b/web/oss/src/components/pages/agents/AgentsPage.tsx @@ -135,11 +135,12 @@ export default function AgentsPage() { scroll: {x: "max-content"}, loading: isLoading, onRow: (record) => ({ - onClick: () => router.push(`${baseAppURL}/${record.workflowId}/overview`), + // Primary click opens the agent in the playground (its main surface), not overview. + onClick: () => goToPlayground(undefined, {appId: record.workflowId}), className: "cursor-pointer", }), }), - [baseAppURL, isLoading, router], + [goToPlayground, isLoading], ) return ( diff --git a/web/oss/src/components/pages/prompts/PromptsPage.tsx b/web/oss/src/components/pages/prompts/PromptsPage.tsx index ed06a41a17..8dc512c8a4 100644 --- a/web/oss/src/components/pages/prompts/PromptsPage.tsx +++ b/web/oss/src/components/pages/prompts/PromptsPage.tsx @@ -22,6 +22,7 @@ import {timeout} from "@/oss/components/pages/app-management/assets/helpers" import useCustomWorkflowConfig from "@/oss/components/pages/app-management/modals/CustomWorkflowModal/hooks/useCustomWorkflowConfig" import {openDeleteAppModalAtom} from "@/oss/components/pages/app-management/modals/DeleteAppModal/store/deleteAppModalStore" import {openEditAppModalAtom} from "@/oss/components/pages/app-management/modals/EditAppModal/store/editAppModalStore" +import {usePlaygroundNavigation} from "@/oss/hooks/usePlaygroundNavigation" import useURL from "@/oss/hooks/useURL" import {useBreadcrumbsEffect} from "@/oss/lib/hooks/useBreadcrumbs" import {waitForAppToStart} from "@/oss/services/api" @@ -69,6 +70,7 @@ const PromptsPage = () => { const {projectId} = useProjectData() const router = useRouter() const {baseAppURL} = useURL() + const {goToPlayground} = usePlaygroundNavigation() const statusData = useAtomValue(appCreationStatusAtom) const setStatusData = useSetAtom(appCreationStatusAtom) const resetAppCreation = useSetAtom(resetAppCreationAtom) @@ -253,7 +255,8 @@ const PromptsPage = () => { return } - handleOpenAppOverview(record.workflowId) + // Primary click opens the prompt in the playground (its main surface), not overview. + goToPlayground(undefined, {appId: record.workflowId}) } const handleBreadcrumbFolderChange = (folderId: string | null) => { diff --git a/web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/index.tsx b/web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/index.tsx index a6c4f953fd..201b2bfc0b 100644 --- a/web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/index.tsx +++ b/web/oss/src/pages/w/[workspace_id]/p/[project_id]/apps/[app_id]/index.tsx @@ -19,18 +19,17 @@ const ensureString = (value: string | string[] | undefined) => { } /** - * `/apps/[app_id]/` → `/apps/[app_id]/{overview|playground}` redirect. + * `/apps/[app_id]/` → `/apps/[app_id]/playground` redirect. * * Fast-path (eng review T2): if the app query already has the workflow - * cached synchronously AND it's an app workflow, redirect to `/overview` + * cached synchronously AND it's an app workflow, redirect to `/playground` * immediately — no skeleton flicker for the 99% case. Apps used to redirect * instantly via the previous implementation; this preserves that behavior. * * Slow-path: cold-load, evaluator workflow, snippet, or not-found cases all * go through `currentWorkflowContextAtom` which waits for both queries to - * settle. Evaluators get `/playground` as default (overview is disabled for - * them per Phase 2). Not-found renders `` instead of - * looping the spinner. + * settle. Playground is the default surface for every workflow kind. Not-found + * renders `` instead of looping the spinner. */ const AppOverviewRedirect = () => { const router = useRouter() @@ -70,11 +69,11 @@ const AppOverviewRedirect = () => { return } - // Fast-path: synchronous app hit → /overview right now. + // Fast-path: synchronous app hit → /playground right now. if (synchronousAppHit) { const destination = `/w/${encodeURIComponent(workspaceId)}/p/${encodeURIComponent( projectId, - )}/apps/${encodeURIComponent(appId)}/overview` + )}/apps/${encodeURIComponent(appId)}/playground` if (router.asPath !== destination) { void router.replace(destination) } @@ -87,11 +86,10 @@ const AppOverviewRedirect = () => { if (ctx.isError) return // page renders skeleton; avoid a wrong-redirect on error if (ctx.isNotFound) return // renders below - // Pick destination based on workflow kind. - const target = ctx.workflowKind === "evaluator" ? "playground" : "overview" + // Playground is the default surface for every workflow kind. const destination = `/w/${encodeURIComponent(workspaceId)}/p/${encodeURIComponent( projectId, - )}/apps/${encodeURIComponent(appId)}/${target}` + )}/apps/${encodeURIComponent(appId)}/playground` if (router.asPath !== destination) { void router.replace(destination) } @@ -105,7 +103,6 @@ const AppOverviewRedirect = () => { ctx.isResolving, ctx.isError, ctx.isNotFound, - ctx.workflowKind, router, ]) diff --git a/web/oss/src/state/app/atoms/fetcher.ts b/web/oss/src/state/app/atoms/fetcher.ts index a3c6fb7271..7831535b17 100644 --- a/web/oss/src/state/app/atoms/fetcher.ts +++ b/web/oss/src/state/app/atoms/fetcher.ts @@ -60,7 +60,7 @@ export const appSwitchHrefAtom = atom((get) => { }) ? ["evaluations"] : rest - return nextRest.length ? `${base}/${nextRest.join("/")}` : `${base}/overview` + return nextRest.length ? `${base}/${nextRest.join("/")}` : `${base}/playground` } }) From 2c7028e27f03d9d7a834efe211a845802234f51e Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Mon, 27 Jul 2026 21:18:20 +0600 Subject: [PATCH 4/7] refactor: update ConnectModelBanner prompt text and remove unused variables --- .../components/ConnectModelBanner.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/web/oss/src/components/AgentChatSlice/components/ConnectModelBanner.tsx b/web/oss/src/components/AgentChatSlice/components/ConnectModelBanner.tsx index 1ea652930a..f004252434 100644 --- a/web/oss/src/components/AgentChatSlice/components/ConnectModelBanner.tsx +++ b/web/oss/src/components/AgentChatSlice/components/ConnectModelBanner.tsx @@ -1,5 +1,3 @@ -import {useRef} from "react" - import {openAgentConfigSectionAtom} from "@agenta/shared/state" import {Button} from "antd" import {useSetAtom} from "jotai" @@ -11,7 +9,7 @@ import {chatPanelMaximizedAtom} from "../state/panelLayout" import RevealCollapse from "./RevealCollapse" /** - * Connect-a-model prompt shown above the composer while the project vault is empty (see `gateActive` + * Set-up-your-key prompt shown above the composer while the project vault is empty (see `gateActive` * on `useAgentModelKeyStatus` — project-wide, not per-provider). The composer is disabled alongside it * (the parent gates on the same status). "Set up credentials" flips the playground to Build and opens * the Model & harness drawer, whose bottom credentials field lets the user add the key without leaving @@ -22,8 +20,6 @@ import RevealCollapse from "./RevealCollapse" * pre-commit onboarding defers the check). */ const ConnectModelBanner = ({ - provider, - providerEntry, gateActive, suppressed = false, }: AgentModelKeyStatus & {suppressed?: boolean}) => { @@ -31,10 +27,6 @@ const ConnectModelBanner = ({ const openConfigSection = useSetAtom(openAgentConfigSectionAtom) const open = !suppressed && gateActive - // Latch the label so the banner keeps its text while it collapses closed (the leave transition - // needs its content to persist through the height animation). - const labelRef = useRef("a model") - if (providerEntry) labelRef.current = providerEntry.title ?? provider ?? "a model" const openCredentials = () => { setChatMaximized(false) @@ -46,9 +38,7 @@ const ConnectModelBanner = ({
- - Connect {labelRef.current} to run this agent with your own key. - + Add your model provider key to run this agent. + + + + +
+ setDeleteConfirmInput(event.target.value)} + placeholder="Organization name" + autoComplete="off" + spellCheck={false} + status={deleteConfirmInput && !isDeleteNameMatch ? "error" : undefined} + autoFocus + /> +
+
+ + + ) +} + +export default OrganizationGeneral diff --git a/web/oss/src/components/pages/settings/assets/navigation.test.ts b/web/oss/src/components/pages/settings/assets/navigation.test.ts index 9f40b72e8a..5f75890d31 100644 --- a/web/oss/src/components/pages/settings/assets/navigation.test.ts +++ b/web/oss/src/components/pages/settings/assets/navigation.test.ts @@ -1,6 +1,11 @@ import {describe, expect, it} from "vitest" -import {resolveSettingsTab, type SettingsAccess} from "./navigation" +import { + getSettingsSidebarTabs, + resolveSettingsTab, + SETTINGS_SCOPES, + type SettingsAccess, +} from "./navigation" const baseAccess: SettingsAccess = { billingEnabled: true, @@ -29,14 +34,58 @@ describe("resolveSettingsTab", () => { ) }) - it("keeps valid tabs that are not shown in the sidebar", () => { + it("keeps the projects tab reachable", () => { expect(resolveSettingsTab("projects", baseAccess)).toBe("projects") }) + it("gives organization owners the general tab in both editions", () => { + expect(resolveSettingsTab("organizationGeneral", {...baseAccess, isEE: false})).toBe( + "organizationGeneral", + ) + expect(resolveSettingsTab("organizationGeneral", {...baseAccess, isOwner: false})).toBe( + "workspace", + ) + }) + it("gates tools and triggers independently", () => { expect(resolveSettingsTab("tools", {...baseAccess, canShowTools: false})).toBe("workspace") expect(resolveSettingsTab("triggers", {...baseAccess, canShowTriggers: false})).toBe( "workspace", ) }) + + it("keeps personal feature flags available in OSS", () => { + const ossAccess = {...baseAccess, isEE: false, isOwner: false} + + expect(resolveSettingsTab("featureFlags", ossAccess)).toBe("featureFlags") + expect(resolveSettingsTab("account", ossAccess)).toBe("workspace") + }) +}) + +describe("settings sidebar scopes", () => { + it("groups tabs by project, organization, and personal scope", () => { + expect(SETTINGS_SCOPES.map(({key}) => key)).toEqual(["project", "organization", "personal"]) + + const tabs = getSettingsSidebarTabs(baseAccess) + const keysForScope = (scope: (typeof SETTINGS_SCOPES)[number]["key"]) => + tabs.filter((tab) => tab.scope === scope).map(({key}) => key) + + expect(keysForScope("project")).toEqual([ + "apiKeys", + "secrets", + "llms", + "tools", + "triggers", + "webhooks", + ]) + expect(keysForScope("organization")).toEqual([ + "organizationGeneral", + "workspace", + "projects", + "organization", + "auditLog", + "billing", + ]) + expect(keysForScope("personal")).toEqual(["account", "featureFlags"]) + }) }) diff --git a/web/oss/src/components/pages/settings/assets/navigation.ts b/web/oss/src/components/pages/settings/assets/navigation.ts index 1d85bd914a..d6403b2224 100644 --- a/web/oss/src/components/pages/settings/assets/navigation.ts +++ b/web/oss/src/components/pages/settings/assets/navigation.ts @@ -9,10 +9,14 @@ export type SettingsTabKey = | "webhooks" | "workspace" | "projects" + | "organizationGeneral" | "organization" | "auditLog" | "billing" | "account" + | "featureFlags" + +export type SettingsScopeKey = "project" | "organization" | "personal" export interface SettingsAccess { billingEnabled: boolean @@ -26,26 +30,36 @@ export interface SettingsAccess { export interface SettingsTabDefinition { key: SettingsTabKey + scope: SettingsScopeKey showInSidebar?: boolean getLabel?: (access: SettingsAccess) => string } export const SETTINGS_TABS: SettingsTabDefinition[] = [ - {key: "apiKeys"}, - {key: "secrets"}, - {key: "llms"}, - {key: "tools"}, - {key: "triggers"}, - {key: "webhooks"}, - {key: "workspace"}, - {key: "projects", showInSidebar: false}, - {key: "organization"}, - {key: "auditLog"}, + {key: "apiKeys", scope: "project"}, + {key: "secrets", scope: "project"}, + {key: "llms", scope: "project"}, + {key: "tools", scope: "project"}, + {key: "triggers", scope: "project"}, + {key: "webhooks", scope: "project"}, + {key: "organizationGeneral", scope: "organization"}, + {key: "workspace", scope: "organization"}, + {key: "projects", scope: "organization"}, + {key: "organization", scope: "organization"}, + {key: "auditLog", scope: "organization"}, { key: "billing", + scope: "organization", getLabel: ({billingEnabled}) => (billingEnabled ? "Usage & Billing" : "Usage"), }, - {key: "account"}, + {key: "account", scope: "personal"}, + {key: "featureFlags", scope: "personal"}, +] + +export const SETTINGS_SCOPES: {key: SettingsScopeKey; title: string}[] = [ + {key: "project", title: "Project"}, + {key: "organization", title: "Organization"}, + {key: "personal", title: "Personal"}, ] const SETTINGS_LABELS: Record, string> = { @@ -57,9 +71,11 @@ const SETTINGS_LABELS: Record, string> = { webhooks: "Webhooks", workspace: "Members", projects: "Projects", + organizationGeneral: "General", organization: "Access & Security", auditLog: "Audit Log", account: "Account", + featureFlags: "Feature flags", } export const isSettingsTabKey = (value: string | null | undefined): value is SettingsTabKey => @@ -79,6 +95,8 @@ export const isSettingsTabVisible = (key: SettingsTabKey, access: SettingsAccess return access.canShowTools case "triggers": return access.canShowTriggers + case "organizationGeneral": + return access.isOwner case "organization": return access.isEE && access.isOwner case "auditLog": diff --git a/web/oss/src/lib/onboarding/atoms.ts b/web/oss/src/lib/onboarding/atoms.ts index 8bf3c1f6ee..7498afa826 100644 --- a/web/oss/src/lib/onboarding/atoms.ts +++ b/web/oss/src/lib/onboarding/atoms.ts @@ -9,6 +9,7 @@ const STORAGE_KEYS = { ACTIVE_USER_ID: "agenta:onboarding:active-user-id", IS_NEW_USER: "is-new-user", NAV_SIMPLIFIED: "nav-simplified", + NAV_SIMPLIFIED_OVERRIDE: "nav-simplified-override", SEEN_TOURS: "seen-tours", } as const @@ -28,6 +29,13 @@ const navSimplifiedDefaultAtomFamily = atomFamily((userId: string) => atomWithStorage(createScopedStorageKey(userId, STORAGE_KEYS.NAV_SIMPLIFIED), false), ) +const navSimplifiedOverrideAtomFamily = atomFamily((userId: string) => + atomWithStorage( + createScopedStorageKey(userId, STORAGE_KEYS.NAV_SIMPLIFIED_OVERRIDE), + null, + ), +) + const seenToursAtomFamily = atomFamily((userId: string) => atomWithStorage>( createScopedStorageKey(userId, STORAGE_KEYS.SEEN_TOURS), @@ -81,6 +89,20 @@ export const navSimplifiedDefaultAtom = atom( }, ) +/** A user's explicit navigation choice. Null preserves their signup-era default. */ +export const navSimplifiedOverrideAtom = atom( + (get) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return null + return get(navSimplifiedOverrideAtomFamily(userId)) + }, + (get, set, next: boolean | null) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return + set(navSimplifiedOverrideAtomFamily(userId), next) + }, +) + /** * Tracks which tours have been seen/completed * diff --git a/web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx b/web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx index 135ccfa17c..b360e8276d 100644 --- a/web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx +++ b/web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx @@ -53,6 +53,11 @@ const Organization = dynamic(() => import("@/oss/components/pages/settings/Organ ssr: false, }) +const OrganizationGeneral = dynamic( + () => import("@/oss/components/pages/settings/Organization/General"), + {ssr: false}, +) + const DeleteAccount = dynamic( () => import("@/oss/components/pages/settings/Account/DeleteAccount"), {ssr: false}, @@ -62,6 +67,11 @@ const Webhooks = dynamic(() => import("@/oss/components/pages/settings/Webhooks/ ssr: false, }) +const FeatureFlags = dynamic( + () => import("@/oss/components/pages/settings/FeatureFlags/FeatureFlags"), + {ssr: false}, +) + interface SettingsProps { AuditLogComponent?: React.ComponentType } @@ -108,31 +118,39 @@ export const Settings: React.FC = ({AuditLogComponent}) => { const isDemoOrg = selectedOrg?.flags?.is_demo ?? false + const buildOrganizationTitle = useCallback( + (label: string) => ( +
+ {label} + + + + Organization ID + + + {isDemoOrg && demo} +
+ ), + [handleCopyOrgId, isDemoOrg, isOrgIdCopied], + ) + const {content, title} = useMemo(() => { switch (resolvedTab) { + case "organizationGeneral": + return { + content: , + title: buildOrganizationTitle( + getSettingsTabLabel("organizationGeneral", settingsAccess), + ), + } case "organization": return { content: , - title: ( -
- {getSettingsTabLabel("organization", settingsAccess)} - - - - Organization ID - - - {isDemoOrg && ( - - demo - - )} -
+ title: buildOrganizationTitle( + getSettingsTabLabel("organization", settingsAccess), ), } case "llms": @@ -173,13 +191,18 @@ export const Settings: React.FC = ({AuditLogComponent}) => { content: , title: getSettingsTabLabel("account", settingsAccess), } + case "featureFlags": + return { + content: , + title: getSettingsTabLabel("featureFlags", settingsAccess), + } default: return { content: , title: getSettingsTabLabel("workspace", settingsAccess), } } - }, [resolvedTab, isOrgIdCopied, handleCopyOrgId, isDemoOrg, settingsAccess, AuditLogComponent]) + }, [resolvedTab, buildOrganizationTitle, settingsAccess, AuditLogComponent]) return ( { * existing users keep the full nav. Stable seam: Phase 2 layers a user override here * (`override ?? default`) without touching consumers. */ -export const advancedNavHiddenAtom = atom((get) => get(navSimplifiedDefaultAtom)) +export const advancedNavHiddenAtom = atom((get) => { + const override = get(navSimplifiedOverrideAtom) + return override ?? get(navSimplifiedDefaultAtom) +}) diff --git a/web/oss/src/state/settings/featureFlags.ts b/web/oss/src/state/settings/featureFlags.ts new file mode 100644 index 0000000000..3408822189 --- /dev/null +++ b/web/oss/src/state/settings/featureFlags.ts @@ -0,0 +1,21 @@ +import {atom} from "jotai" +import {atomFamily, atomWithStorage} from "jotai/utils" + +import {onboardingStorageUserIdAtom} from "@/oss/lib/onboarding/atoms" + +const playgroundInspectorAtomFamily = atomFamily((userId: string) => + atomWithStorage(`agenta:settings:${userId}:playground-inspector`, false), +) + +export const playgroundInspectorEnabledAtom = atom( + (get) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return false + return get(playgroundInspectorAtomFamily(userId)) + }, + (get, set, next: boolean) => { + const userId = get(onboardingStorageUserIdAtom) + if (!userId) return + set(playgroundInspectorAtomFamily(userId), next) + }, +) From ce54191e870fb5c6f1040aa9cae1e36d99fc2cca Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 27 Jul 2026 22:57:04 +0200 Subject: [PATCH 7/7] fix(frontend): stop a failed org delete from escaping as an unhandled 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 --- .../components/pages/settings/Organization/General.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/oss/src/components/pages/settings/Organization/General.tsx b/web/oss/src/components/pages/settings/Organization/General.tsx index 3d4e87cae5..d602313d3b 100644 --- a/web/oss/src/components/pages/settings/Organization/General.tsx +++ b/web/oss/src/components/pages/settings/Organization/General.tsx @@ -134,7 +134,13 @@ const OrganizationGeneral = () => { const handleDelete = useCallback(async () => { if (!organizationId || !isDeleteNameMatch) return - await deleteMutation.mutateAsync() + // antd hands onOk straight to the OK button's onClick, so a rejection here would + // escape as an unhandled promise; the mutation's onError already surfaces it. + try { + await deleteMutation.mutateAsync() + } catch { + return + } message.success("Organization deleted") // Latent: GET /organizations list omits default_workspace (details-only field) — typed as-is.