Skip to content

feat(frontend): @agenta/settings + @agenta/settings-ui — the settings spine leaves the app - #5885

Draft
ardaerzin wants to merge 18 commits into
pkg/agent-overview-bodyfrom
pkg/settings-spine
Draft

feat(frontend): @agenta/settings + @agenta/settings-ui — the settings spine leaves the app#5885
ardaerzin wants to merge 18 commits into
pkg/agent-overview-bodyfrom
pkg/settings-spine

Conversation

@ardaerzin

@ardaerzin ardaerzin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Settings was the last large surface still owned entirely by the app layer, and /m had no
settings at all. This lane builds the spine both editions and the mobile app hang their pages on.

  • @agenta/settings — the headless half: the API-keys service, the theme controller, the shared
    page/tab model.
  • @agenta/settings-ui — the rendered half: the settings shell, Preferences, Account, the secret
    and vault tables, the webhooks page, the Projects table.
  • @agenta/entities gains the profile and webhook entity layers; @agenta/entities/profile is
    what lets /m show a real Account tab.
  • APIKeys splits into useApiKeys + ApiKeysPage, so the mobile app can reuse the hook without
    the desktop page.
  • The desktop theme provider now runs on the shared controller rather than its own.
  • /m gets a real settings page — shared shell, shared Preferences — plus a settings rail, and
    the settings packages are registered with Tailwind so their classes are not purged.

Twelve commits; the last is the lockfile for settings-ui's entity-ui and react-query deps.

Not run in a browser — static gates only (pnpm lint-fix 24/24, tsc --noEmit clean
for @agenta/shared, ui, entities, entity-ui, settings-ui, oss, ee, mobile).

Stacked on pkg/agent-overview-body; review only this lane's diff.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Error Error Aug 11, 2026 5:11am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5edeaca3-d037-4896-a72a-4321dda59600

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a functional, responsive settings experience to mobile.
    • Added shared settings navigation with access-aware tabs and descriptions.
    • Added reusable pages for preferences, account management, API keys, projects, secrets, and webhooks.
    • Added project and webhook management actions, including creation, editing, testing, deletion, and activation controls.
    • Added consistent light, dark, and system theme preferences across applications.
  • Bug Fixes

    • Improved handling of signed-out profiles, API-key loading, date formatting, and webhook test results.

Walkthrough

Changes

The PR adds shared @agenta/settings and @agenta/settings-ui packages. It centralizes settings navigation, access control, theme handling, API-key management, projects, secrets, webhooks, account, and preferences. OSS and mobile settings now consume the shared implementations.

Shared platform

Layer / File(s) Summary
Settings contracts and shared utilities
web/packages/agenta-settings/..., web/packages/agenta-shared/..., web/packages/agenta-ui/...
Adds shared settings navigation, access control, API-key hooks, static-table behavior, date utilities, and theme-mode handling.
Shared entity APIs and state
web/packages/agenta-entities/...
Adds profile and project APIs plus typed webhook APIs, Jotai state, mutations, delivery handling, and exports.
Reusable settings UI
web/packages/agenta-settings-ui/...
Adds reusable pages and tables for account, preferences, API keys, projects, secrets, webhooks, theme selection, and layout.
OSS settings integration
web/oss/src/components/pages/settings/..., web/oss/src/services/..., web/oss/src/state/...
Replaces local OSS settings implementations with shared packages while retaining OSS-specific dialogs and callbacks.
Mobile settings integration
web/mobile/src/features/settings/..., web/mobile/src/styles/globals.css
Replaces the mobile settings placeholder with URL-driven preferences and account navigation. Tailwind and Next.js now process the shared packages.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the extraction of settings functionality into shared packages and the related desktop and mobile changes.
Title check ✅ Passed The title clearly identifies the extraction of settings functionality into the shared @agenta/settings and @agenta/settings-ui packages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pkg/settings-spine

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.

@ardaerzin ardaerzin changed the title PLACEHOLDER feat(frontend): @agenta/settings + @agenta/settings-ui — the settings spine leaves the app Aug 10, 2026
@ardaerzin
ardaerzin changed the base branch from pkg/agent-overview-layout to pkg/agent-overview-body August 10, 2026 07:06
@ardaerzin
ardaerzin force-pushed the pkg/agent-overview-body branch from fe74b64 to 2a8c549 Compare August 10, 2026 14:59
@ardaerzin
ardaerzin force-pushed the pkg/agent-overview-body branch from 2a8c549 to 4218be4 Compare August 10, 2026 15:14
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

🧹 Nitpick comments (12)
web/packages/agenta-settings-ui/src/AccountPage.tsx (1)

126-131: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Enforce the typed-email gate inside onConfirm.

The component documents that it owns the typed-email gate. onConfirm calls onDeleteAccount() without checking confirmed, so the gate depends entirely on each host disabling its confirm button. A host that wires the callback to an always-enabled button deletes the account with no typed confirmation. Deletion is irreversible, so guard it here as well.

🛡️ Proposed guard
-                      onConfirm: () => void deletion.onDeleteAccount(),
+                      onConfirm: () => {
+                          if (!confirmed || deleting) return
+                          void deletion.onDeleteAccount()
+                      },
web/packages/agenta-settings-ui/src/SettingsPageShell.tsx (1)

40-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a named export for consistency.

AccountPage, ApiKeysPage, PreferencesPage, and ThemePicker all use named exports. This file uses a default export. A named export keeps the package surface uniform and makes re-exports in src/index.ts symmetric.

♻️ Proposed change
-const SettingsPageShell = ({
+export const SettingsPageShell = ({
-export default SettingsPageShell

Also applies to: 107-107

web/packages/agenta-settings-ui/src/projects/ProjectsPage.tsx (1)

19-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export ProjectFormValues.

ProjectDialogState<ProjectFormValues> appears in the exported ProjectsPageProps signature, but ProjectFormValues stays module-private. A host that implements renderCreateDialog or renderRenameDialog cannot name the value type.

♻️ Proposed fix
-interface ProjectFormValues {
+export interface ProjectFormValues {
     name: string
     make_default?: boolean
 }

Then re-export it from web/packages/agenta-settings-ui/src/index.ts:

-export {ProjectsPage, type ProjectsPageProps, type ProjectDialogState} from "./projects/ProjectsPage"
+export {
+    ProjectsPage,
+    type ProjectsPageProps,
+    type ProjectDialogState,
+    type ProjectFormValues,
+} from "./projects/ProjectsPage"

Also applies to: 43-45

web/packages/agenta-settings-ui/src/secrets/SecretProviderTable.tsx (1)

47-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the ProviderRow interface out of the component body.

The type declaration inside the component adds no value and prevents reuse by the host. Declare it at module scope next to ProviderDialogState. The same pattern exists in NamedSecretTable.tsx at Lines 58-61.

web/packages/agenta-settings-ui/src/webhooks/WebhooksPage.tsx (3)

16-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge the three imports from @agenta/entities/webhook.

The file imports from the same module three times. One statement is enough and the style pipeline reports a formatting failure for this file.

♻️ Proposed refactor
-import {
-    WEBHOOK_TEST_FAILURE_MESSAGE,
-    handleTestResult,
-} from "`@agenta/entities/webhook`"
-import type {WebhookProvider, WebhookSubscription} from "`@agenta/entities/webhook`"
-import {setWebhookActiveAtom, testWebhookAtom, webhooksAtom} from "`@agenta/entities/webhook`"
-import {
-    editingWebhookAtom,
-    isWebhookDrawerOpenAtom,
-    webhookToDeleteAtom,
-} from "`@agenta/entities/webhook`"
+import {
+    WEBHOOK_TEST_FAILURE_MESSAGE,
+    editingWebhookAtom,
+    handleTestResult,
+    isWebhookDrawerOpenAtom,
+    setWebhookActiveAtom,
+    testWebhookAtom,
+    webhookToDeleteAtom,
+    webhooksAtom,
+    type WebhookProvider,
+    type WebhookSubscription,
+} from "`@agenta/entities/webhook`"

As per coding guidelines: "Run pnpm lint-fix from the web directory before committing."

Sources: Coding guidelines, Pipeline failures


50-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the repository regex.

Inside a character class, \/ is a useless escape. eslint no-useless-escape reports this pattern. Use /repos\/([^/]+\/[^/]+)\//.


146-151: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Memoize the per-row toggle handler.

handleToggle(record) returns a new function on every render, so ActiveToggle re-renders for every row on any state change, including each keystroke in the search input. Pass the webhook id and one stable callback instead.

♻️ Proposed refactor
-    const handleToggle = useCallback(
-        (webhook: WebhookSubscription) => async (next: boolean) => {
-            await setWebhookActive({id: webhook.id, active: next})
-        },
-        [setWebhookActive],
-    )
+    const handleToggle = useCallback(
+        async (id: string, next: boolean) => {
+            await setWebhookActive({id, active: next})
+        },
+        [setWebhookActive],
+    )
-                            <ActiveToggle
-                                active={isWebhookActive(record)}
-                                onToggle={handleToggle(record)}
+                            <ActiveToggle
+                                active={isWebhookActive(record)}
+                                onToggle={(next: boolean) => handleToggle(record.id, next)}

As per coding guidelines: "avoid unstable inline functions and objects, especially in lists."

Also applies to: 222-232

Source: Coding guidelines

web/oss/src/components/pages/settings/Projects/index.tsx (1)

9-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared ProjectFormValues type instead of duplicating it.

web/packages/agenta-settings-ui/src/projects/ProjectsPage.tsx lines 9-12 declare the identical interface. Two copies can drift, and the OSS forms feed onSubmit of the shared page. Export the type from @agenta/settings-ui and import it here.

web/oss/src/components/pages/settings/assets/navigation.ts (1)

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

Narrow this re-export to the navigation surface.

export * from "@agenta/settings" exposes the whole package through a navigation module. Hooks such as useApiKeys and useStaticTable, the API-key service, and the React access helpers all become reachable from settings/assets/navigation. That widens the module surface and pulls React and API code into consumers that previously imported only navigation constants.

Re-export the navigation symbols explicitly, or update the consumers to import from @agenta/settings and delete this shim.

web/packages/agenta-entities/src/webhook/atoms.ts (1)

40-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider pruning webhookDeliveriesAtomFamily.

atomFamily retains one atom for every webhookSubscriptionId passed to it, including deleted subscriptions. The family has no remove call and no custom shouldRemove, so the retained atoms and their query subscriptions live for the whole session. The key space is bounded by the webhook count, so the impact is small. Add webhookDeliveriesAtomFamily.setShouldRemove(...) or call remove(id) after a delete if the settings page can churn subscriptions.

web/oss/src/components/pages/settings/Preferences/Preferences.tsx (1)

24-55: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize the theme and flags props.

Line 26 rebuilds the options array on every render. Lines 30-54 rebuild the flags array and every flag object on every render. Both props are arrays of objects, so PreferencesPage receives new identities on each render even when no flag value changed. This defeats any memoization inside the shared component.

♻️ Proposed memoization
-import {PreferencesPage} from "`@agenta/settings-ui`"
+import {useMemo} from "react"
+
+import {PreferencesPage} from "`@agenta/settings-ui`"
 import {useAtom, useAtomValue, useSetAtom} from "jotai"
+    const theme = useMemo(
+        () => ({
+            options: THEME_OPTIONS.map(({mode, label}) => ({mode, label})),
+            mode: themeMode,
+            onSelect: (mode: string) => toggleAppTheme(mode as ThemeMode),
+        }),
+        [themeMode, toggleAppTheme],
+    )
+
+    const flags = useMemo(
+        () => [
+            {
+                key: "classic-mode",
+                title: "Classic mode",
+                description: "Show all platform areas in the navigation.",
+                enabled: !advancedNavHidden,
+                onChange: (enabled: boolean) => setNavSimplifiedOverride(!enabled),
+            },
+            {
+                key: "voice-input",
+                title: "Voice input",
+                description: "Dictate messages in the agent chat.",
+                enabled: agentVoiceInputEnabled,
+                onChange: setAgentVoiceInputEnabled,
+            },
+            {
+                key: "playground-inspector",
+                title: "Playground inspector",
+                description:
+                    "Show controls for inspecting Playground sessions and individual turns.",
+                enabled: playgroundInspectorEnabled,
+                onChange: setPlaygroundInspectorEnabled,
+                badge: "DEBUG",
+            },
+        ],
+        [
+            advancedNavHidden,
+            setNavSimplifiedOverride,
+            agentVoiceInputEnabled,
+            setAgentVoiceInputEnabled,
+            playgroundInspectorEnabled,
+            setPlaygroundInspectorEnabled,
+        ],
+    )
+
     return (
-        <PreferencesPage
-            theme={{
-                options: THEME_OPTIONS.map(({mode, label}) => ({mode, label})),
-                mode: themeMode,
-                onSelect: (mode) => toggleAppTheme(mode as ThemeMode),
-            }}
-            flags={[
-                {
-                    key: "classic-mode",
-                    title: "Classic mode",
-                    description: "Show all platform areas in the navigation.",
-                    enabled: !advancedNavHidden,
-                    onChange: (enabled) => setNavSimplifiedOverride(!enabled),
-                },
-                {
-                    key: "voice-input",
-                    title: "Voice input",
-                    description: "Dictate messages in the agent chat.",
-                    enabled: agentVoiceInputEnabled,
-                    onChange: setAgentVoiceInputEnabled,
-                },
-                {
-                    key: "playground-inspector",
-                    title: "Playground inspector",
-                    description:
-                        "Show controls for inspecting Playground sessions and individual turns.",
-                    enabled: playgroundInspectorEnabled,
-                    onChange: setPlaygroundInspectorEnabled,
-                    badge: "DEBUG",
-                },
-            ]}
-        />
+        <PreferencesPage theme={theme} flags={flags} />
     )

Based on learnings, this comment relies on the coding guideline "Memoize inline arrays containing objects or JSX when passing them as props to avoid unnecessary rerenders."

Source: Coding guidelines

web/oss/src/services/webhooks/types.ts (1)

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

Replace the wildcard shim re-exports with explicit named re-exports. These five shims use export * where the removed local modules exported a specific, narrow set of symbols. Two consequences follow. First, export * performs no compile-time export-parity check at the shim, so a renamed or dropped symbol in a shared package surfaces only at each call site. Second, the three webhook shims now re-export the whole @agenta/entities/webhook barrel, so services/webhooks/api.ts, services/webhooks/types.ts, and state/webhooks/atoms.ts expose byte-identical surfaces. A caller can import an atom from the types path or a type from the state path, which erases the services/state boundary. web/oss/src/components/pages/settings/hooks/useStaticTable.ts already uses a named re-export and is the pattern to follow.

  • web/oss/src/services/webhooks/types.ts#L1-L1: re-export only the webhook types with export type {...} from "@agenta/entities/webhook" so this module carries no runtime atoms or axios code.
  • web/oss/src/services/webhooks/api.ts#L1-L1: re-export only the webhook API functions by name.
  • web/oss/src/state/webhooks/atoms.ts#L1-L1: re-export only the webhook atoms by name.
  • web/oss/src/services/project/index.ts#L1-L1: re-export only the project API symbols that OSS callers use.
  • web/oss/src/lib/helpers/dateTimeHelper/index.ts#L1-L1: re-export only the date helper functions that OSS callers use.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ea59ad7d-9f53-4896-9fd2-6bdea8b21aa7

📥 Commits

Reviewing files that changed from the base of the PR and between 4218be4 and 6cf00f1.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (72)
  • web/mobile/next.config.ts
  • web/mobile/package.json
  • web/mobile/src/features/settings/SettingsScreen.tsx
  • web/mobile/src/styles/globals.css
  • web/oss/next.config.ts
  • web/oss/package.json
  • web/oss/src/components/Layout/ThemeContextProvider.tsx
  • web/oss/src/components/Webhooks/utils/handleTestResult.ts
  • web/oss/src/components/pages/settings/APIKeys/APIKeys.tsx
  • web/oss/src/components/pages/settings/APIKeys/assets/constants.ts
  • web/oss/src/components/pages/settings/Account/DeleteAccount.tsx
  • web/oss/src/components/pages/settings/Preferences/Preferences.tsx
  • web/oss/src/components/pages/settings/Preferences/components/ThemePicker.tsx
  • web/oss/src/components/pages/settings/Projects/index.tsx
  • web/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsx
  • web/oss/src/components/pages/settings/Vault/NamedSecretTable/index.tsx
  • web/oss/src/components/pages/settings/Webhooks/Webhooks.tsx
  • web/oss/src/components/pages/settings/assets/navigation.ts
  • web/oss/src/components/pages/settings/components/SettingsPageShell.tsx
  • web/oss/src/components/pages/settings/hooks/useSettingsAccess.ts
  • web/oss/src/components/pages/settings/hooks/useStaticTable.ts
  • web/oss/src/lib/helpers/dateTimeHelper/index.ts
  • web/oss/src/services/api.ts
  • web/oss/src/services/apiKeys/api/index.ts
  • web/oss/src/services/project/index.ts
  • web/oss/src/services/project/types.ts
  • web/oss/src/services/webhooks/api.ts
  • web/oss/src/services/webhooks/types.ts
  • web/oss/src/state/webhooks/atoms.ts
  • web/oss/src/state/webhooks/state.ts
  • web/oss/tailwind.config.ts
  • web/packages/agenta-entities/package.json
  • web/packages/agenta-entities/src/profile/index.ts
  • web/packages/agenta-entities/src/project/api.ts
  • web/packages/agenta-entities/src/project/index.ts
  • web/packages/agenta-entities/src/project/types.ts
  • web/packages/agenta-entities/src/webhook/api.ts
  • web/packages/agenta-entities/src/webhook/atoms.ts
  • web/packages/agenta-entities/src/webhook/handleTestResult.ts
  • web/packages/agenta-entities/src/webhook/index.ts
  • web/packages/agenta-entities/src/webhook/state.ts
  • web/packages/agenta-entities/src/webhook/types.ts
  • web/packages/agenta-settings-ui/eslint.config.mjs
  • web/packages/agenta-settings-ui/package.json
  • web/packages/agenta-settings-ui/src/AccountPage.tsx
  • web/packages/agenta-settings-ui/src/ApiKeysPage.tsx
  • web/packages/agenta-settings-ui/src/PreferencesPage.tsx
  • web/packages/agenta-settings-ui/src/SettingsPageShell.tsx
  • web/packages/agenta-settings-ui/src/ThemePicker.tsx
  • web/packages/agenta-settings-ui/src/index.ts
  • web/packages/agenta-settings-ui/src/projects/ProjectsPage.tsx
  • web/packages/agenta-settings-ui/src/secrets/NamedSecretTable.tsx
  • web/packages/agenta-settings-ui/src/secrets/SecretProviderTable.tsx
  • web/packages/agenta-settings-ui/src/webhooks/WebhooksPage.tsx
  • web/packages/agenta-settings-ui/tsconfig.json
  • web/packages/agenta-settings/eslint.config.mjs
  • web/packages/agenta-settings/package.json
  • web/packages/agenta-settings/src/access.tsx
  • web/packages/agenta-settings/src/api/apiKeys.ts
  • web/packages/agenta-settings/src/index.ts
  • web/packages/agenta-settings/src/navigation.ts
  • web/packages/agenta-settings/src/useApiKeys.ts
  • web/packages/agenta-settings/src/useStaticTable.ts
  • web/packages/agenta-settings/tests/unit/navigation.test.ts
  • web/packages/agenta-settings/tsconfig.json
  • web/packages/agenta-settings/vitest.config.ts
  • web/packages/agenta-shared/package.json
  • web/packages/agenta-shared/src/utils/dateTime/dayjs.ts
  • web/packages/agenta-shared/src/utils/dateTime/index.ts
  • web/packages/agenta-ui/package.json
  • web/packages/agenta-ui/src/theme/index.ts
  • web/packages/agenta-ui/src/theme/useThemeMode.ts
💤 Files with no reviewable changes (2)
  • web/oss/src/components/pages/settings/APIKeys/assets/constants.ts
  • web/oss/src/components/pages/settings/Preferences/components/ThemePicker.tsx

Comment on lines +121 to +141
<div className="min-w-0 flex-1 overflow-y-auto">
<SettingsPageShell
variant="form"
title={getSettingsTabLabel(active, access)}
description={getSettingsTabDescription(active, access)}
>
{active === "preferences" ? (
<PreferencesPage
theme={{
options: THEME_OPTIONS,
mode: themeMode,
onSelect: (mode) => setMode(mode as typeof themeMode),
}}
/>
) : (
// Identity only: deleting an account is an EE capability and
// this app has no EE surface.
<AccountPage username={user?.username} email={user?.email} />
)}
</SettingsPageShell>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the mobile theme declares the antd-style color tokens and --ant-* variables.
fd -t f 'globals.css' web/mobile/src --exec cat -n {}
rg -nP -- '--(ant|ag)-(font-size-heading|line-height-heading|font-weight-strong)' web/mobile | head -30
rg -nP -- '\bcolorText\b|\bcolorSplit\b|\bcolorBorderSecondary\b|\bcolorPrimary\b' web/mobile --glob '!**/node_modules/**' | head -30

Repository: Agenta-AI/agenta

Length of output: 21799


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- settings component locations ---'
fd -t f . web | rg '/(SettingsPageShell|PreferencesPage|ThemePicker)\.(tsx|ts)$'
printf '%s\n' '--- referenced classes and ant variables ---'
rg -n -C 3 -- 'text-colorText|text-colorTextSecondary|border-colorBorderSecondary|bg-colorSplit|border-colorPrimary|--ant-font-size-heading-3|--ant-line-height-heading-3|--ant-font-weight-strong' web/packages web/mobile --glob '*.{ts,tsx,css}'
printf '%s\n' '--- generated token declarations ---'
sed -n '1,220p' web/mobile/src/styles/theme.generated.css

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- settings files ---'
rg --files web/packages | rg '/(SettingsPageShell|PreferencesPage|ThemePicker)\.(tsx|ts)$'
printf '%s\n' '--- settings references ---'
rg -n -C 4 -- 'text-colorText|text-colorTextSecondary|border-colorBorderSecondary|bg-colorSplit|border-colorPrimary|--ant-font-size-heading-3|--ant-line-height-heading-3|--ant-font-weight-strong' web/packages/agenta-settings web/packages/agenta-settings-ui web/mobile/src/features/settings
printf '%s\n' '--- ant font variables in mobile ---'
rg -n -- '--ant-font-size-heading-3|--ant-line-height-heading-3|--ant-font-weight-strong' web/mobile/src web/packages/agenta-settings web/packages/agenta-settings-ui
printf '%s\n' '--- mobile theme token declarations ---'
sed -n '1,210p' web/mobile/src/styles/theme.generated.css

Repository: Agenta-AI/agenta

Length of output: 30841


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- mobile Ant Design setup ---'
rg -n -C 4 -- 'ConfigProvider|cssVar|theme\s*=|antd' web/mobile/src web/mobile --glob '*.{ts,tsx}'
printf '%s\n' '--- all declarations of the referenced variables ---'
rg -n -- '--ant-font-size-heading-3|--ant-line-height-heading-3|--ant-font-weight-strong' web --glob '*.{css,ts,tsx}'
printf '%s\n' '--- mobile stylesheet imports ---'
rg -n -C 2 -- '`@agenta/ui/theme-variables`|theme.generated|antd|globals.css' web/mobile/src web/mobile --glob '*.{css,ts,tsx}'

Repository: Agenta-AI/agenta

Length of output: 14224


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- referenced variable declarations ---'
rg --glob '*.css' --glob '*.ts' --glob '*.tsx' -n -- '--ant-font-size-heading-3|--ant-line-height-heading-3|--ant-font-weight-strong' web
printf '%s\n' '--- mobile stylesheet imports ---'
rg --glob '*.css' --glob '*.ts' --glob '*.tsx' -n -C 2 -- '`@agenta/ui/theme-variables`|theme.generated|globals.css' web/mobile
printf '%s\n' '--- mobile dark-mode setup ---'
rg --glob '*.ts' --glob '*.tsx' -n -C 3 -- 'classList.*dark|themeMode|prefers-color-scheme|setMode' web/mobile/src

Repository: Agenta-AI/agenta

Length of output: 7146


Define the missing heading tokens for mobile.

web/mobile/src/styles/globals.css maps the referenced color tokens in both themes, but mobile has no Ant Design runtime to define --ant-font-size-heading-3, --ant-line-height-heading-3, or --ant-font-weight-strong. Define these variables or replace the inline styles in SettingsPageShell with mobile-supported values.

Comment on lines +86 to +90
// The stored preference, the theme it resolves to, the OS listener and the `.dark` class
// all live in @agenta/ui/theme, so this app and /m read and write one contract. What stays
// here is the part only this app has: antd's ConfigProvider and its cssVar key class.
const {themeMode, resolved, setMode} = useThemeMode()
const appTheme: ThemeType = resolved === "dark" ? ThemeMode.Dark : ThemeMode.Light

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline web/packages/agenta-ui/src/theme/useThemeMode.ts --items all
rg -n -C 5 'matchMedia|const sync|setThemeMode\("system"\)|resolved' \
  web/packages/agenta-ui/src/theme/useThemeMode.ts

Repository: Agenta-AI/agenta

Length of output: 2023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- useThemeMode.ts ---'
cat -n web/packages/agenta-ui/src/theme/useThemeMode.ts

printf '%s\n' '--- hook consumers ---'
rg -n -C 3 'useThemeMode\(' web --glob '*.{ts,tsx}'

printf '%s\n' '--- related tests ---'
rg -n -C 4 'ThemeMode|useThemeMode|prefers-color-scheme|matchMedia' web/packages web/oss --glob '*.{test,spec}.{ts,tsx}' --glob '*.{ts,tsx}' | head -n 240

printf '%s\n' '--- relevant diff summary ---'
git diff --stat -- web/oss/src/components/Layout/ThemeContextProvider.tsx web/packages/agenta-ui/src/theme/useThemeMode.ts
git diff -- web/oss/src/components/Layout/ThemeContextProvider.tsx web/packages/agenta-ui/src/theme/useThemeMode.ts | sed -n '1,240p'

Repository: Agenta-AI/agenta

Length of output: 27420


Make system mode react to OS preference changes.

The matchMedia listener calls setThemeMode("system") while the state is already "system". React ignores the unchanged update, so resolved remains stale after an OS theme change. Store the resolved system mode in state or use an external-store subscription, and add a test for this case.

Source: Coding guidelines

Comment on lines +26 to +56
renderCreateDialog={({open, onClose, onSubmit, pending}) => (
<EnhancedModal
title="Create project"
open={open}
okText="Create"
onCancel={() => {
onClose()
createForm.resetFields()
}}
onOk={() => createForm.submit()}
confirmLoading={pending}
>
<Form form={createForm} layout="vertical" onFinish={onSubmit}>
<Form.Item
label="Project name"
name="name"
rules={[{required: true, message: "Please enter a project name"}]}
>
<Input placeholder="e.g. Production evaluation" autoFocus />
</Form.Item>
<Form.Item
label="Make default project"
name="make_default"
valuePropName="checked"
extra="The default project is used whenever a workspace is selected from the navigation."
>
<Switch />
</Form.Item>
</Form>
</EnhancedModal>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset createForm when the dialog closes, not only on cancel.

createForm.resetFields() runs only in onCancel. After a successful create, the dialog closes through onSubmit and the form keeps the entered name and the make_default switch value. The next time the user opens the create dialog, the previous values are still present.

Use afterClose to reset the form on every close path.

🐛 Proposed fix
                 <EnhancedModal
                     title="Create project"
                     open={open}
                     okText="Create"
                     onCancel={() => {
                         onClose()
-                        createForm.resetFields()
                     }}
+                    afterClose={() => createForm.resetFields()}
                     onOk={() => createForm.submit()}
                     confirmLoading={pending}
                 >

Apply the same change to the rename dialog at lines 65-68.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
renderCreateDialog={({open, onClose, onSubmit, pending}) => (
<EnhancedModal
title="Create project"
open={open}
okText="Create"
onCancel={() => {
onClose()
createForm.resetFields()
}}
onOk={() => createForm.submit()}
confirmLoading={pending}
>
<Form form={createForm} layout="vertical" onFinish={onSubmit}>
<Form.Item
label="Project name"
name="name"
rules={[{required: true, message: "Please enter a project name"}]}
>
<Input placeholder="e.g. Production evaluation" autoFocus />
</Form.Item>
<Form.Item
label="Make default project"
name="make_default"
valuePropName="checked"
extra="The default project is used whenever a workspace is selected from the navigation."
>
<Switch />
</Form.Item>
</Form>
</EnhancedModal>
)}
renderCreateDialog={({open, onClose, onSubmit, pending}) => (
<EnhancedModal
title="Create project"
open={open}
okText="Create"
onCancel={() => {
onClose()
}}
afterClose={() => createForm.resetFields()}
onOk={() => createForm.submit()}
confirmLoading={pending}
>
<Form form={createForm} layout="vertical" onFinish={onSubmit}>
<Form.Item
label="Project name"
name="name"
rules={[{required: true, message: "Please enter a project name"}]}
>
<Input placeholder="e.g. Production evaluation" autoFocus />
</Form.Item>
<Form.Item
label="Make default project"
name="make_default"
valuePropName="checked"
extra="The default project is used whenever a workspace is selected from the navigation."
>
<Switch />
</Form.Item>
</Form>
</EnhancedModal>
)}

Comment on lines +1 to +8
import {axios, getAgentaApiUrl} from "@agenta/shared/api"
import type {User} from "@agenta/shared/types"
import {useQuery} from "@tanstack/react-query"

/** GET the signed-in user. `ignoreAxiosError` suppresses the global toast for callers that
* handle 401 themselves (the desktop treats it as "signed out", not a failure). */
export const fetchProfile = async (ignoreAxiosError = false) =>
axios.get(`${getAgentaApiUrl()}/profile`, {_ignoreError: ignoreAxiosError} as never)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a '^resources\.ts$' web/packages/agenta-sdk
rg -n -C 4 'profile|project|safeParseWithLogging' \
  web/packages/agenta-sdk web/packages/agenta-entities web/packages/agenta-shared

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target files ---'
cat -n web/packages/agenta-entities/src/profile/index.ts
cat -n web/packages/agenta-entities/src/project/api.ts

printf '%s\n' '--- SDK resource declarations ---'
rg -n -i -C 3 'profile|project|workspace' web/packages/agenta-sdk/src/resources.ts web/packages/agenta-sdk/src \
  -g '*.ts' -g '!**/generated/**' | head -n 240

printf '%s\n' '--- boundary validation and schemas ---'
rg -n -C 3 'safeParseWithLogging|profile.*Schema|project.*Schema|ProjectsResponse|User' \
  web/packages/agenta-entities/src/profile web/packages/agenta-entities/src/project \
  web/packages/agenta-entities/src/shared web/packages/agenta-shared/src \
  -g '*.ts' | head -n 300

Repository: Agenta-AI/agenta

Length of output: 34479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- resource client surface ---'
cat -n web/packages/agenta-sdk/src/resources.ts | sed -n '1,125p'

printf '%s\n' '--- generated project client methods and types ---'
fd -a -i 'projects|profile|users|workspaces' web/packages/agenta-sdk web/packages \
  -t f | head -n 120
rg -n -C 4 'class ProjectsClient|listProjects|createProject|updateProject|deleteProject|getProject|profile' \
  web/packages/agenta-sdk web/packages -g '*.ts' -g '*.tsx' | head -n 320

printf '%s\n' '--- project and user schemas/types ---'
cat -n web/packages/agenta-entities/src/project/types.ts
rg -n -C 3 'export.*User|userSchema|project.*Schema|projects.*Schema|ProjectsResponse' \
  web/packages/agenta-shared web/packages/agenta-entities web/packages/agenta-sdk \
  -g '*.ts' | head -n 260

Repository: Agenta-AI/agenta

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- generated ProjectsClient API ---'
cat -n web/packages/agenta-api-client/src/generated/api/resources/projects/client/Client.ts | sed -n '1,360p'

printf '%s\n' '--- generated UsersClient API ---'
cat -n web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts | sed -n '1,110p'

printf '%s\n' '--- resource exports and client options ---'
rg -n -C 2 'UsersClient|ProjectsClient|resources/users|resources/projects' \
  web/packages/agenta-api-client/src web/packages/agenta-sdk/src -g '*.ts' | head -n 220

printf '%s\n' '--- generated response definitions ---'
cat -n web/packages/agenta-api-client/src/generated/api/types/ProjectsResponse.ts | sed -n '1,160p'
rg -n -C 2 'Profile|UserResponse|User\b' web/packages/agenta-api-client/src/generated/api/types \
  -g '*.ts' | head -n 180

Repository: Agenta-AI/agenta

Length of output: 48281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

targets = {
    "profile": Path("web/packages/agenta-entities/src/profile/index.ts"),
    "project": Path("web/packages/agenta-entities/src/project/api.ts"),
}
resources = Path("web/packages/agenta-sdk/src/resources.ts").read_text()
projects_client = Path(
    "web/packages/agenta-api-client/src/generated/api/resources/projects/client/Client.ts"
).read_text()
users_client = Path(
    "web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts"
).read_text()

for name, path in targets.items():
    text = path.read_text()
    print(f"{name}: raw_axios={bool(re.search(r'\\baxios\\.(get|post|patch|delete)\\b', text))}")
    print(f"{name}: safe_parse={('safeParseWithLogging' in text)}")

print(f"resources: has_projects_accessor={bool(re.search(r'function getProjectsClient\\s*\\(', resources))}")
print(f"resources: has_users_accessor={bool(re.search(r'function getUsersClient\\s*\\(', resources))}")

for method in ("getProjects", "getProject", "createProject", "updateProject", "deleteProject"):
    match = re.search(rf"public {method}\\b[\\s\\S]*?(?=\\n    public |\\Z)", projects_client)
    print(f"projects_client.{method}: queryParams={bool(match and 'requestOptions?.queryParams' in match.group(0))}")

print(f"users_client.fetchUserProfile: returns_unknown={bool(re.search(r'fetchUserProfile\\([\\s\\S]*?HttpResponsePromise<unknown>', users_client))}")
print(f"projects_client: runtime_zod_validation={bool(re.search(r'\\b(zod|safeParse|parseWith)\\b', projects_client))}")
print(f"users_client: runtime_zod_validation={bool(re.search(r'\\b(zod|safeParse|parseWith)\\b', users_client))}")
PY

Repository: Agenta-AI/agenta

Length of output: 441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

targets = {
    "profile": Path("web/packages/agenta-entities/src/profile/index.ts"),
    "project": Path("web/packages/agenta-entities/src/project/api.ts"),
}
resources = Path("web/packages/agenta-sdk/src/resources.ts").read_text()
projects_client = Path(
    "web/packages/agenta-api-client/src/generated/api/resources/projects/client/Client.ts"
).read_text()
users_client = Path(
    "web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts"
).read_text()

axios_pattern = re.compile(r"\baxios\.(get|post|patch|delete)\b")
accessor_projects = re.compile(r"function getProjectsClient\s*\(")
accessor_users = re.compile(r"function getUsersClient\s*\(")
zod_pattern = re.compile(r"\b(zod|safeParse|parseWith)\b")

for name, path in targets.items():
    text = path.read_text()
    print(f"{name}: raw_axios={bool(axios_pattern.search(text))}")
    print(f"{name}: safe_parse={'safeParseWithLogging' in text}")

print(f"resources: has_projects_accessor={bool(accessor_projects.search(resources))}")
print(f"resources: has_users_accessor={bool(accessor_users.search(resources))}")

for method in ("getProjects", "getProject", "createProject", "updateProject", "deleteProject"):
    match = re.search(rf"public {method}\b[\s\S]*?(?=\n    public |\Z)", projects_client)
    print(
        f"projects_client.{method}: "
        f"queryParams={bool(match and 'requestOptions?.queryParams' in match.group(0))}"
    )

profile_returns_unknown = re.search(
    r"fetchUserProfile\([\s\S]*?HttpResponsePromise<unknown>", users_client
)
print(f"users_client.fetchUserProfile: returns_unknown={bool(profile_returns_unknown)}")
print(f"projects_client: runtime_zod_validation={bool(zod_pattern.search(projects_client))}")
print(f"users_client: runtime_zod_validation={bool(zod_pattern.search(users_client))}")
PY

Repository: Agenta-AI/agenta

Length of output: 704


Migrate both API boundaries to Fern and runtime validation.

  • web/packages/agenta-entities/src/profile/index.ts: add getUsersClient to web/packages/agenta-sdk/src/resources.ts, call fetchUserProfile(), and validate the response with safeParseWithLogging.
  • web/packages/agenta-entities/src/project/api.ts: use getProjectsClient() for all five operations, pass workspace_id through {queryParams: {...}}, and validate every response with safeParseWithLogging.
📍 Affects 2 files
  • web/packages/agenta-entities/src/profile/index.ts#L1-L8 (this comment)
  • web/packages/agenta-entities/src/project/api.ts#L13-L55

Source: Coding guidelines

Comment on lines +22 to +36
export const useProfile = ({enabled = true}: UseProfileOptions = {}) => {
const query = useQuery<User | null>({
queryKey: ["profile"],
queryFn: async () => {
try {
const res = await fetchProfile(true)
return (res?.data as User) ?? null
} catch (error) {
if ((error as {response?: {status?: number}})?.response?.status === 401) return null
throw error
}
},
enabled,
staleTime: 60_000,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 --glob '*.{ts,tsx}' 'atomWithQuery|useProfile|queryKey:\s*\["profile"\]' web/packages

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- profile files ---'
fd -i 'profile' web/packages/agenta-entities
echo '--- target outline ---'
ast-grep outline web/packages/agenta-entities/src/profile/index.ts
echo '--- target source ---'
cat -n web/packages/agenta-entities/src/profile/index.ts
echo '--- profile symbol usages ---'
rg -n -C 3 --glob '*.{ts,tsx}' '\b(useProfile|fetchProfile|profileQueryAtom|profile)\b' web/packages/agenta-entities web/apps web/packages 2>/dev/null | head -n 500
echo '--- query package configuration ---'
rg -n -C 3 'jotai-tanstack-query|`@tanstack/react-query`|jotai' web/packages/agenta-entities/package.json web/package.json web/pnpm-lock.yaml 2>/dev/null | head -n 200

Repository: Agenta-AI/agenta

Length of output: 45761


🏁 Script executed:

#!/bin/bash
set -u

echo '--- exact profile symbols ---'
rg -n -C 5 --glob '*.{ts,tsx}' \
  '(^|[^A-Za-z0-9_])(useProfile|fetchProfile|profileQueryAtom|profileAtom)([^A-Za-z0-9_]|$)' \
  web/packages web/apps 2>/dev/null | head -n 400

echo '--- package manifests ---'
for f in web/packages/agenta-entities/package.json web/package.json; do
  if [ -f "$f" ]; then
    echo "### $f"
    cat "$f"
  fi
done

echo '--- profile-related state files ---'
fd -i -t f 'profile|user' web/packages web/apps 2>/dev/null | head -n 200

echo '--- query atom and API validation patterns ---'
rg -n -C 4 --glob '*.{ts,tsx}' \
  'atomWithQuery|safeParseWithLogging|resources|users\.' \
  web/packages/agenta-entities/src web/packages/agenta-shared/src web/packages/agenta-sdk/src 2>/dev/null | head -n 500

echo '--- SDK resource definitions ---'
if [ -f web/packages/agenta-sdk/src/resources.ts ]; then
  cat -n web/packages/agenta-sdk/src/resources.ts
fi

Repository: Agenta-AI/agenta

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- generated users client ---'
cat -n web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts | sed -n '1,95p'

echo '--- generated users exports ---'
fd -t f . web/packages/agenta-api-client/src/generated/api/resources/users | sort | head -n 80
rg -n -C 3 'User|Profile|profile\(' web/packages/agenta-api-client/src/generated/api/resources/users | head -n 250

echo '--- resource accessor implementation ---'
cat -n web/packages/agenta-sdk/src/resources.ts | sed -n '1,260p'

echo '--- validation export ---'
rg -n -C 5 'safeParseWithLogging' web/packages/agenta-entities/src/shared web/packages/agenta-entities/src/index.ts web/packages/agenta-shared/src 2>/dev/null | head -n 180

echo '--- compact atomWithQuery examples in entities ---'
for f in \
  web/packages/agenta-entities/src/webhook/atoms.ts \
  web/packages/agenta-entities/src/gatewayTool/hooks/useToolConnectionsQuery.ts \
  web/packages/agenta-entities/src/gatewayTrigger/hooks/useTriggerSubscriptions.ts
do
  if [ -f "$f" ]; then
    echo "### $f"
    cat -n "$f" | sed -n '1,90p'
  fi
done

Repository: Agenta-AI/agenta

Length of output: 37994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- User type and schemas ---'
rg -n -C 6 --glob '*.{ts,tsx}' \
  'export (interface|type) User\b|userSchema|UserSchema|profileSchema|ProfileSchema|z\.object\(' \
  web/packages/agenta-shared/src web/packages/agenta-entities/src web/oss 2>/dev/null | head -n 400

echo '--- API boundary patterns for unknown Fern responses ---'
rg -n -C 6 --glob '*.{ts,tsx}' \
  'fetch[A-Za-z]+\(\)|HttpResponsePromise<unknown>|\.data.*safeParseWithLogging|safeParseWithLogging\(.*data' \
  web/packages/agenta-entities/src web/packages/agenta-sdk/src 2>/dev/null | head -n 300

Repository: Agenta-AI/agenta

Length of output: 50374


Move the profile query to a validated shared query atom.

Define an atomWithQuery for ["profile"] and preserve enabled and staleTime. Replace the raw Axios call with a getUsersClient() accessor from @agenta/sdk/resources (add it to web/packages/agenta-sdk/src/resources.ts) and validate fetchUserProfile().data with safeParseWithLogging instead of casting to User. Map Fern 401 errors to null to preserve the current behavior.

Source: Coding guidelines

Comment on lines +1 to +9
export * from "./navigation"
export {useStaticTable} from "./useStaticTable"
export {
SettingsAccessProvider,
useSettingsAccess,
CLOSED_SETTINGS_ACCESS,
} from "./access"
export {fetchAllListApiKeys, createApiKey, deleteApiKey} from "./api/apiKeys"
export {useApiKeys, type ApiKey, type ApiKeyRow, type UseApiKeysOptions} from "./useApiKeys"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Format this export file.

Prettier rejects the current file. Format it before merge.

As per coding guidelines, run pnpm lint-fix from web before committing.

🧰 Tools
🪛 GitHub Actions: 11 - check code styling / 0_TypeScript format.txt

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix this file.

Sources: Coding guidelines, Pipeline failures

Comment on lines +58 to +115
const list = useCallback(() => {
if (!canView || !workspaceId.trim()) {
setKeys([])
return
}
setListing(true)
fetchAllListApiKeys(workspaceId)
.then((res) => {
setKeys(
((res.data ?? []) as ApiKey[]).map((key) => ({
...key,
key: key.prefix,
id: key.prefix,
})),
)
})
.catch(console.error)
.finally(() => setListing(false))
}, [canView, workspaceId])

useEffect(() => {
list()
}, [list])

const remove = useCallback(
async (prefix: string) => {
if (!canEdit) return
if (!(await confirmDelete())) return
setDeleting(true)
try {
await deleteApiKey(prefix)
setKeys((current) => current.filter((key) => key.prefix !== prefix))
} catch (error) {
console.error(error)
} finally {
setDeleting(false)
}
},
[canEdit, confirmDelete],
)

const create = useCallback(async () => {
if (!canEdit) return
if (!workspaceId.trim()) {
onWorkspacePending?.()
return
}
setCreating(true)
try {
const {data} = await createApiKey(workspaceId)
list()
onCreated(data as string)
} catch (error) {
console.error(error)
} finally {
setCreating(false)
}
}, [canEdit, list, onCreated, onWorkspacePending, workspaceId])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect established query-atom conventions and API-key consumers.
rg -n -C 3 --glob '*.{ts,tsx}' 'atomWithQuery|queryKey|invalidateQueries' web/packages web/oss
rg -n -C 5 --glob '*.{ts,tsx}' 'useApiKeys|fetchAllListApiKeys|createApiKey|deleteApiKey' web

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- useApiKeys.ts ---'
cat -n web/packages/agenta-settings/src/useApiKeys.ts | sed -n '1,180p'

printf '%s\n' '--- API-key definitions and consumers ---'
rg -n -C 4 --glob '*.{ts,tsx}' \
  'fetchAllListApiKeys|createApiKey|deleteApiKey|useApiKeys|projectIdAtom|workspaceId' \
  web/packages/agenta-settings web/packages/agenta-entities web/oss/src/components/pages/settings \
  | head -n 500

printf '%s\n' '--- package dependencies and exports ---'
rg -n -C 3 'jotai-tanstack-query|`@tanstack/react-query`|agenta-settings' \
  web/packages/agenta-settings/package.json web/package.json web/pnpm-lock.yaml 2>/dev/null | head -n 200

Repository: Agenta-AI/agenta

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- settings package configuration ---'
cat web/packages/agenta-settings/package.json
printf '%s\n' '--- workspace package source files ---'
git ls-files web/packages/agenta-settings/src

printf '%s\n' '--- query-client access patterns in settings packages ---'
rg -n -C 5 --glob '*.{ts,tsx}' \
  'queryClientAtom|useQueryClient|atomWithQuery|projectIdAtom|getDefaultStore' \
  web/packages/agenta-settings web/packages/agenta-settings-ui

printf '%s\n' '--- project atom definition and updates ---'
rg -n -C 5 --glob '*.{ts,tsx}' \
  'export .*projectIdAtom|const projectIdAtom|projectIdAtom.*=' \
  web/packages/agenta-shared web/oss/src | head -n 200

Repository: Agenta-AI/agenta

Length of output: 7843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- API-key endpoint implementation ---'
rg -n -C 6 --glob '*.{py,ts,tsx}' \
  'class .*ApiKey|def .*api.?key|/keys/|workspace_id.*project_id|delete.*key' \
  . | head -n 500

printf '%s\n' '--- focused consumer behavior ---'
cat -n web/oss/src/components/pages/settings/APIKeys/APIKeys.tsx | sed -n '1,130p'

printf '%s\n' '--- standalone stale-response reproduction ---'
node - <<'JS'
const pending = new Map()
let visible = []

function list(workspaceId) {
  const request = new Promise((resolve) => pending.set(workspaceId, resolve))
  request.then((data) => {
    visible = data
    console.log(`resolved ${workspaceId}:`, JSON.stringify(visible))
  })
}

list("workspace-A")
list("workspace-B")
pending.get("workspace-B")(["B-prefix"])
pending.get("workspace-A")(["A-prefix"])

if (JSON.stringify(visible) !== JSON.stringify(["A-prefix"])) {
  throw new Error("Expected the older workspace-A response to overwrite workspace-B")
}
console.log("stale workspace response overwrites current state: reproduced")
JS

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- stale-response reproduction ---'
node - <<'JS'
const pending = new Map()
let visible = []

function list(workspaceId) {
  const request = new Promise((resolve) => pending.set(workspaceId, resolve))
  request.then((data) => {
    visible = data
    console.log(`resolved ${workspaceId}: ${JSON.stringify(visible)}`)
  })
}

list("workspace-A")
list("workspace-B")
pending.get("workspace-B")(["B-prefix"])
pending.get("workspace-A")(["A-prefix"])

if (JSON.stringify(visible) !== JSON.stringify(["A-prefix"])) {
  throw new Error("The stale-response race was not reproduced")
}
console.log("stale workspace-A response overwrites workspace-B state")
JS

printf '%s\n' '--- exact key API scope ---'
cat -n web/packages/agenta-settings/src/api/apiKeys.ts

Repository: Agenta-AI/agenta

Length of output: 826


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const pending = new Map()
let visible = []

function list(workspaceId) {
  const request = new Promise((resolve) => pending.set(workspaceId, resolve))
  request.then((data) => {
    visible = data
    console.log(`resolved ${workspaceId}: ${JSON.stringify(visible)}`)
  })
}

async function main() {
  list("workspace-A")
  list("workspace-B")
  pending.get("workspace-B")(["B-prefix"])
  pending.get("workspace-A")(["A-prefix"])
  await Promise.resolve()

  if (JSON.stringify(visible) !== JSON.stringify(["A-prefix"])) {
    throw new Error("The stale-response race was not reproduced")
  }
  console.log("stale workspace-A response overwrites workspace-B state")
}

main()
JS

printf '%s\n' '--- exact key API scope ---'
cat -n web/packages/agenta-settings/src/api/apiKeys.ts

Repository: Agenta-AI/agenta

Length of output: 1810


Move API-key listing to a workspace- and project-scoped query atom.

The current useEffect can let a response for the previous workspace overwrite the current key list. Key the query by workspaceId and projectId, pass the same projectId to the request, gate it with enabled, and invalidate it after create and delete. Use a package that provides atomWithQuery or add the required dependency.

Source: Coding guidelines

Comment on lines +24 to +45
export const parseDate = ({
date,
inputFormat = "YYYY-MM-DD H:mm:sssAZ",
}: {
date: dayjs.ConfigType
inputFormat?: string
}) => {
return dayjs(date, inputFormat)
}

export const formatDay = ({
date,
inputFormat = "YYYY-MM-DD H:mm:ssAZ",
outputFormat = "DD MMM YYYY",
}: {
date: dayjs.ConfigType
inputFormat?: string
outputFormat?: string
}): string => {
const formatsToTry = inputFormat
? [inputFormat, ...FALLBACK_FORMATS.filter((format) => format !== inputFormat)]
: FALLBACK_FORMATS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 --glob '*.{ts,tsx}' '\b(parseDate|formatDay)\s*\(' web
rg -n -C 2 --glob '*.{ts,tsx,json}' 'created_at|updated_at|timestamp|datetime' web/packages web/oss

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dateTime utility ---'
fd -i '^index\.ts$' web/packages/agenta-shared/src/utils/dateTime --exec sh -c 'cat -n "$1"' sh {}

printf '%s\n' '--- dateTime directory files ---'
git ls-files web/packages/agenta-shared/src/utils/dateTime

printf '%s\n' '--- Day.js imports and plugin setup ---'
rg -n -C 3 'dayjs|customParseFormat|utc|timezone' \
  web/packages/agenta-shared/src/utils/dateTime \
  web/packages/agenta-shared/src \
  web/package.json web/pnpm-lock.yaml package.json pnpm-lock.yaml 2>/dev/null | head -n 240

printf '%s\n' '--- direct imports and calls ---'
rg -n -C 4 'from ["'\''][^"'\'']*dateTime[^"'\'']*["'\'']|import .*parseDate|import .*formatDay|\b(parseDate|formatDay)\s*\(' \
  web --glob '*.{ts,tsx}' \
  | head -n 400

printf '%s\n' '--- timestamp examples near date parsing ---'
rg -n -C 3 'created_at|updated_at|timestamp|datetime|start_time|end_time' \
  web/oss web/packages/agenta-shared \
  --glob '*.{ts,tsx}' \
  | rg -i 'dateTime|parseDate|formatDay|toISOString|created_at|updated_at|timestamp|datetime|start_time|end_time' \
  | head -n 300

Repository: Agenta-AI/agenta

Length of output: 17619


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- parseDate references ---'
rg -n -C 3 '\bparseDate\b' web --glob '*.ts' --glob '*.tsx' || true

printf '%s\n' '--- formatDay references ---'
rg -n -C 3 '\bformatDay\b' web --glob '*.ts' --glob '*.tsx' || true

printf '%s\n' '--- dateTime exports ---'
rg -n -C 5 'dateTime|formatDate24|formatDate|formatDay|parseDate' \
  web/packages/agenta-shared/src/index.ts \
  web/packages/agenta-shared/src/utils/index.ts \
  web/packages/agenta-shared/package.json 2>/dev/null || true

printf '%s\n' '--- tests mentioning date parsing ---'
rg -n -C 4 'parseDate|formatDay|FALLBACK_FORMATS|YYYY-MM-DD H:mm:sssAZ|customParseFormat' \
  web --glob '*test*' --glob '*spec*' --glob '*.ts' --glob '*.tsx' \
  | head -n 400 || true

printf '%s\n' '--- local Day.js runtime availability ---'
for p in web/node_modules/dayjs node_modules/dayjs; do
  if [ -d "$p" ]; then
    echo "$p exists"
  else
    echo "$p unavailable"
  fi
done

printf '%s\n' '--- package metadata ---'
cat web/packages/agenta-shared/package.json

Repository: Agenta-AI/agenta

Length of output: 50374


🌐 Web query:

Day.js customParseFormat plugin token s ss S SS SSS A Z strict parsing

💡 Result:

The Day.js CustomParseFormat plugin enables parsing of date strings using specific formats [1][2]. The tokens you inquired about are used to match and parse components of a date string as follows: - s: Seconds (0-59) [3][4] - ss: Seconds (00-59) [3][4] - S: Hundreds of milliseconds (1-digit, 0-9) [3][4] - SS: Tens of milliseconds (2-digits, 00-99) [3][4] - SSS: Milliseconds (3-digits, 000-999) [3][4] - A: Post or ante meridiem (AM/PM), upper-case [3][4] - Z: Offset from UTC (e.g., -05:00) [3][4] Strict parsing is enabled by passing true as the third argument to the dayjs constructor (e.g., dayjs(input, format, true)) [1][2]. When strict parsing is active, Day.js requires that the input string and the provided format match exactly, including delimiters [5][6]. Note that strict mode performs validation to ensure the parsed result is a valid date [6]. Top results: [1][3][4][2][6]

Citations:


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- timestamp-producing code and examples ---'
rg -n -C 3 \
  'datetime\.now|datetime\.utcnow|isoformat\(|toISOString\(|created_at|updated_at|start_time|timestamp' \
  . \
  --glob '*.py' --glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.ts' --glob '*.tsx' \
  | rg -v 'generated|node_modules|pnpm-lock|package-lock|yarn.lock' \
  | head -n 500 || true

printf '%s\n' '--- dateTime helper bridge ---'
fd -i 'dateTimeHelper' web --type d --exec sh -c 'find "$1" -maxdepth 2 -type f -print -exec sed -n "1,180p" {} \;' sh {} \; 2>/dev/null | head -n 300 || true

printf '%s\n' '--- package metadata and test locations ---'
sed -n '1,100p' web/packages/agenta-shared/package.json
git ls-files web/packages/agenta-shared | rg -i 'test|spec|date|time' || true

printf '%s\n' '--- dependency availability ---'
test -d web/node_modules && echo 'web/node_modules exists' || echo 'web/node_modules unavailable'
test -d web/node_modules/dayjs && echo 'web/node_modules/dayjs exists' || echo 'web/node_modules/dayjs unavailable'

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

curl -fsSL https://unpkg.com/dayjs@1.11.20/dayjs.min.js -o "$tmpdir/dayjs.min.js"
curl -fsSL https://unpkg.com/dayjs@1.11.20/plugin/customParseFormat.js -o "$tmpdir/customParseFormat.js"
curl -fsSL https://unpkg.com/dayjs@1.11.20/plugin/utc.js -o "$tmpdir/utc.js"

node - "$tmpdir" <<'JS'
const path = require("path")
const root = process.argv[2]
const dayjs = require(path.join(root, "dayjs.min.js"))
const customParseFormat = require(path.join(root, "customParseFormat.js"))
const utc = require(path.join(root, "utc.js"))

dayjs.extend(customParseFormat)
dayjs.extend(utc)

const cases = [
  ["normal ISO milliseconds with offset", "2024-01-02 13:14:15.678+00:00"],
  ["normal ISO seconds with offset", "2024-01-02 13:14:15+00:00"],
  ["declared default-shaped value", "2024-01-02 1:14:15PM+00:00"],
  ["ISO microseconds without offset", "2024-01-02T13:14:15.123456"],
]

const formats = [
  "YYYY-MM-DD H:mm:sssAZ",
  "YYYY-MM-DD HH:mm:ss.SSSZ",
  "YYYY-MM-DDTHH:mm:ss.SSSSSS",
]

for (const [label, value] of cases) {
  console.log(label)
  for (const format of formats) {
    const parsed = dayjs(value, format)
    const strict = dayjs(value, format, true)
    console.log(JSON.stringify({
      value,
      format,
      looseValid: parsed.isValid(),
      looseOutput: parsed.isValid() ? parsed.toISOString() : null,
      strictValid: strict.isValid(),
    }))
  }
}
JS

printf '%s\n' '--- dateTime helper bridge ---'
cat -n web/oss/src/lib/helpers/dateTimeHelper/index.ts

printf '%s\n' '--- focused API serialization references ---'
rg -n -C 5 \
  'model_dump|jsonable_encoder|isoformat|toISOString|created_at|updated_at' \
  api/src api/ee/src \
  --glob '*.py' \
  | rg -i 'response|serializer|json|created_at|updated_at|isoformat|model_dump|jsonable_encoder' \
  | head -n 350 || true

Repository: Agenta-AI/agenta

Length of output: 8027


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

curl -fsSL https://unpkg.com/dayjs@1.11.20/dayjs.min.js -o "$tmpdir/dayjs.min.js"
curl -fsSL https://unpkg.com/dayjs@1.11.20/plugin/customParseFormat.js -o "$tmpdir/customParseFormat.js"
curl -fsSL https://unpkg.com/dayjs@1.11.20/plugin/utc.js -o "$tmpdir/utc.js"

node - "$tmpdir" <<'JS'
const path = require("path")
const root = process.argv[2]
const dayjs = require(path.join(root, "dayjs.min.js"))
const customParseFormat = require(path.join(root, "customParseFormat.js"))
const utc = require(path.join(root, "utc.js"))

dayjs.extend(customParseFormat)
dayjs.extend(utc)

const FALLBACK_FORMATS = [
  "YYYY-MM-DD H:mm:ssAZ",
  "YYYY-MM-DD H:mm:sssAZ",
  "YYYY-MM-DD HH:mm:ss.SSSZ",
  "YYYY-MM-DD HH:mm:ss.SSS",
  "YYYY-MM-DD HH:mm:ssZ",
  "YYYY-MM-DD HH:mm:ss",
  "YYYY-MM-DDTHH:mm:ss.SSSSSS",
  "YYYY-MM-DDTHH:mm:ss.SSSZ",
  "YYYY-MM-DDTHH:mm:ssZ",
  "YYYY-MM-DDTHH:mm:ss",
]

function formatDay(date, inputFormat = "YYYY-MM-DD H:mm:ssAZ", outputFormat = "HH:mm:ss.SSSZ") {
  const formatsToTry = inputFormat
    ? [inputFormat, ...FALLBACK_FORMATS.filter((format) => format !== inputFormat)]
    : FALLBACK_FORMATS

  for (const format of formatsToTry) {
    const parsed = dayjs.utc(date, format)
    if (parsed.isValid()) {
      return {format, value: parsed.format(outputFormat)}
    }
  }

  const direct = dayjs.utc(date)
  return {format: "direct", value: direct.isValid() ? direct.format(outputFormat) : ""}
}

for (const value of [
  "2024-01-02 13:14:15.678+00:00",
  "2024-01-02T13:14:15.678000Z",
  "2024-01-02T13:14:15.678Z",
]) {
  console.log(JSON.stringify({value, result: formatDay(value)}))
}
JS

Repository: Agenta-AI/agenta

Length of output: 489


Use a valid strict default format for parseDate.

YYYY-MM-DD H:mm:sssAZ can parse 2024-01-02 13:14:15.678+00:00 as 13:15:07.000Z. The first formatDay fallback also drops fractional seconds. Use the API’s actual ISO format, enable strict parsing, and add fixtures for each supported timestamp shape.

Comment on lines +6 to +18
export const THEME_STORAGE_KEY = "agenta-theme"

/** usehooks-ts JSON-encodes its values; the desktop wrote this key that way first. */
const read = (): ThemeModeValue => {
if (typeof window === "undefined") return "system"
try {
const raw = window.localStorage.getItem(THEME_STORAGE_KEY)
if (!raw) return "system"
const value = raw.charAt(0) === '"' ? (JSON.parse(raw) as string) : raw
return value === "light" || value === "dark" ? value : "system"
} catch {
return "system"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'agenta-theme|THEME_STORAGE_KEY|atomWithStorage|useThemeMode' web

Repository: Agenta-AI/agenta

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- useThemeMode.ts ---'
cat -n web/packages/agenta-ui/src/theme/useThemeMode.ts

printf '%s\n' '--- theme consumers and boot scripts ---'
rg -n -C 5 'THEME_STORAGE_KEY|agenta-theme|useThemeMode|matchMedia|resolved' \
  web/packages/agenta-ui/src/theme \
  web/oss/src/pages/_document.tsx \
  web/mobile/src/pages/_document.tsx \
  web/oss/src/components/Layout/ThemeContextProvider.tsx \
  web/mobile/src/features/settings/SettingsScreen.tsx

printf '%s\n' '--- Jotai dependency declarations ---'
rg -n -C 2 '"jotai"|jotai/utils|`@agenta/ui/theme`' \
  web/package.json web/packages/agenta-ui/package.json web/pnpm-lock.yaml 2>/dev/null || true

Repository: Agenta-AI/agenta

Length of output: 18781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let themeMode = "system"
let prefersDark = false
let renderCount = 0
let resolved = prefersDark ? "dark" : "light"

function render() {
  renderCount += 1
  resolved = themeMode === "system" ? (prefersDark ? "dark" : "light") : themeMode
}

function setThemeMode(next) {
  if (Object.is(themeMode, next)) return false
  themeMode = next
  render()
  return true
}

render()
const before = {themeMode, resolved, renderCount}
prefersDark = true
const changed = setThemeMode("system")
const after = {themeMode, resolved, renderCount}

console.log(JSON.stringify({before, changed, after}, null, 2))
if (changed || after.resolved !== "light" || after.renderCount !== before.renderCount) {
  process.exit(1)
}
JS

printf '%s\n' '--- all theme key references ---'
rg -n 'agenta-theme|THEME_STORAGE_KEY|themeInitScript' web \
  -g '!**/node_modules/**' \
  -g '!**/dist/**' \
  -g '!**/build/**'

Repository: Agenta-AI/agenta

Length of output: 2181


Use a shared persisted theme atom and update OS synchronization.

Replace direct localStorage access with atomWithStorage and use an agenta:-prefixed key. Migrate agenta-theme and update both boot scripts so the initial document theme and controller use the same preference.

When themeMode is "system", update the resolved theme directly from the media-query event. Calling setThemeMode("system") does not re-render when the state already has that value.

Source: Coding guidelines

Comment on lines +50 to +57
// Follow the OS while the choice is "system" — otherwise the page only flips on reload.
useEffect(() => {
if (themeMode !== "system" || typeof window === "undefined") return
const query = window.matchMedia("(prefers-color-scheme: dark)")
const sync = () => setThemeMode("system")
query.addEventListener("change", sync)
return () => query.removeEventListener("change", sync)
}, [themeMode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 --glob '*.{ts,tsx}' 'useThemeMode|prefers-color-scheme|matchMedia' web
fd -a -i 'theme*.test.*' web

Repository: Agenta-AI/agenta

Length of output: 10729


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="web/packages/agenta-ui/src/theme/useThemeMode.ts"
ast-grep outline "$file"
cat -n "$file"
printf '\n--- theme consumers ---\n'
rg -n -C 5 'resolved|setMode|THEME_STORAGE_KEY|agenta-theme|localStorage' \
  web/packages/agenta-ui/src/theme web/oss/src/components/Layout/ThemeContextProvider.tsx \
  web/oss/src/pages/_document.tsx web/mobile/src/pages/_document.tsx

Repository: Agenta-AI/agenta

Length of output: 13682


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

source = Path("web/packages/agenta-ui/src/theme/useThemeMode.ts").read_text()
required = [
    'const [themeMode, setThemeMode] = useState<ThemeModeValue>("system")',
    'const resolved: "light" | "dark" =',
    'themeMode === "system" ? (prefersDark() ? "dark" : "light") : themeMode',
    'const sync = () => setThemeMode("system")',
    '}, [resolved])',
]
missing = [item for item in required if item not in source]
if missing:
    raise SystemExit(f"missing expected source fragments: {missing}")

# Model React's state bailout: an update to the identical primitive value
# does not produce the render needed to re-evaluate prefersDark().
state = "system"
renders = 1
os_is_dark = False
resolved = "dark" if os_is_dark else "light"

os_is_dark = True
next_state = "system"
if next_state != state:
    state = next_state
    renders += 1
    resolved = "dark" if os_is_dark else "light"

print({
    "listener_update": "system -> system",
    "state_changed": state != next_state,
    "renders_after_change": renders,
    "resolved_after_change": resolved,
    "expected_resolved_after_change": "dark",
    "bug_reproduced": resolved != "dark",
})
PY

Repository: Agenta-AI/agenta

Length of output: 345


Update the resolved theme after an OS appearance change.

When themeMode === "system", sync sets the state to its current value. React can skip this update, so resolved and the .dark class remain stale until another render. Store the media-query result in state and derive resolved from it.

Source: Coding guidelines

… spine leaves the app

The tab model, the static-table helper and the page shell were already pure: no
antd, no @/oss. They move verbatim (git mv, so 111's featureFlags->preferences
rename keeps its history) and OSS keeps its three import paths as re-exports, so
none of the 13 tab files move.

Access is injected rather than read: useSettingsAccess needs edition detection,
project permissions and org state, none of which exist in a package. The package
owns the SettingsAccess shape and the visibility rules; each host computes the
flags. A host with no provider gets CLOSED_SETTINGS_ACCESS, so a missing provider
hides gated tabs instead of exposing them.
Both were presentation over host state, so both extract cleanly once de-antd'd:
Typography -> tokens, Switch -> Radix, Radio -> RadioGroup, Divider -> a rule,
danger Button -> variant=destructive, --ant-color-error* -> colorError* tokens.

Theme is shared because it is a per-viewer choice both apps honour; the
experiment flags stay host-supplied, since a flag only exists where it ships.
AccountPage keeps the typed-email gate but delegates the dialog through
renderConfirm, so no host can ship a one-click account delete.
First settings extraction with a data half rather than presentation only. The
service was 46 lines over axios + the project id, both already in
@agenta/shared/api, so it moves with an import swap. OSS re-exports it, keeping
one implementation for its three consumers — one of which is in EE, so this is
also the first settings piece to cross editions.

The APIKeys page itself is untouched; splitting its fetch/permissions/dialogs
into a headless hook plus a view is the next unit.
/m's settings was a placeholder saying to use the desktop. It now renders the
SHARED page shell and Preferences tab, with the title and description read from
the shared tab model so the copy cannot drift from the desktop's.

useThemeMode extracts the half of the desktop's ThemeContextProvider both apps
need — the stored preference, the theme it resolves to, and the .dark class —
keyed off the same storage entry, so a viewer's choice follows them between
surfaces. The antd ConfigProvider half stays in the app.

Preferences only, deliberately: it is the one tab needing no profile, org or
permission state. Access stays CLOSED so nothing edition-gated can leak in
before /m can compute real flags.
…ccount tab

The signed-in user had no package source, so /m could not render Account.
fetchProfile was a three-line GET and User already lived in @agenta/shared/types,
so both move into entities with a useProfile hook beside them; OSS re-exports the
fetcher, keeping one implementation. The hook stays thinner than the desktop's
profile atom, which also persists to disk, gates a fanout and redirects — app
concerns, not entity ones.

AccountPage's deletion becomes optional as a pair (action + dialog): deleting an
account is an EE capability that tears down owned orgs, and /m has no EE surface,
so it renders identity only instead of a button that cannot work.
…troller

Two implementations of read-agenta-theme / resolve / toggle-.dark existed once /m
gained its own. The state half now comes from @agenta/ui/theme; what stays is the
part only this app has — antd's ConfigProvider, the darkAlgorithm token config and
the agenta cssVar key class.

Public API is unchanged (ThemeMode, ThemeContext, useAppTheme, getDeviceTheme), so
no consumer moves. useLocalStorage, useLazyEffect, useState and the local
getAppTheme fell out with the state and are gone.

Behaviour note: appTheme was seeded synchronously from storage at first render;
the hook reads after mount instead, because reading localStorage during render is
a hydration mismatch. The document boot script has already applied the class, so
there is no flash.
The tab interleaved fetching, permissions, two dialogs and the table in one
component. useApiKeys owns the list and the verbs; the parts that are genuinely
the host's arrive as callbacks — confirmDelete resolves from its own dialog, and
onCreated receives the secret, because a key is returned once and the host has to
reveal and offer to copy it there and then.

ApiKeysPage is the view, off antd (Alert/Button/Tooltip onto @agenta/ui). The
Loading enum in assets/constants.ts went with the old multi-flag loading state:
the hook returns listing/creating/deleting.
…s-ui

Both were already package-backed on the data side (useVaultSecret from
@agenta/entities/secret), so what remained was de-antd'ing them and lifting the
dialogs out. The model-registry dialogs and the configure-secret modal become
slots: they belong to the registry, not to settings, and each host renders its
own.

dateTimeHelper moves to @agenta/shared/utils/dateTime on the way — both tables
need formatDay, and it has 21 consumers across oss and ee, which all keep their
import path through a re-export.
… /m a settings rail

Two defects, both mine.

Tailwind never scanned @agenta/settings-ui — it is in neither mobile's @source
list nor oss's content globs — so every class in those components was dropped:
no width cap, no grid, no card chrome. That is why the theme picker rendered as
full-bleed stacked bars. Same trap 7551ab30cf fixed for the earlier packages.

And /m had no settings navigation at all: it stacked Preferences and Account down
one scroll. It now renders the desktop's own tab model — SETTINGS_SCOPES groups,
shared labels — as a rail, one tab at a time, with ?tab= keeping it linkable. The
rail is narrowed to the tabs this app implements so nothing dead-ends.
…ge into settings-ui

The whole data half moves, not just the view: api, types, atoms, the drawer state
and the test-result handler, all onto @agenta/shared's axios, queryClient and
projectIdAtom. Eleven OSS files import the old service and state paths and keep
working through stubs.

WebhooksPage is antd-free in @agenta/settings-ui; the drawer, the delete dialog
and the one-time secret reveal are slots, since each belongs to the host.
…in settings-ui

The service consolidates onto one transport: OSS ran GETs through its own fetch
client and writes through axios, which a package cannot reach and which had no
behavioural reason. All five calls now use @agenta/shared's axios and its auth
interceptor; fetchAllProjects still answers [] on 401, as its callers expect.

The create/rename/delete dialogs stay with the host: they are antd Form with
validation rules, and settings-ui is antd-free by contract. The page owns the
table, the toolbar and the mutations and takes the dialogs as slots.

/m's own fetchProjects still exists and is the better implementation — it retries
after a session refresh. Folding that in would change desktop auth behaviour, so
it stays a follow-up rather than riding along with a table move.
The media-query listener was inert. Its handler was setThemeMode("system"), set
while the mode already WAS "system" — the effect only ran in that case — so React
bailed out on Object.is, nothing re-rendered, and `resolved`, which called
prefersDark() during render, never recomputed. An OS light/dark switch was
silently swallowed on both the desktop (ThemeContextProvider reads this hook) and
/m.

Hold the OS preference in state instead and derive `resolved` from it, so a
`change` event is a real state transition. The subscription is unconditional now
rather than only while the choice is "system", so switching back to "system"
resolves against a current value. SSR discipline is unchanged: the new state
starts `false` on both sides of hydration for the same reason themeMode starts
"system" — the boot script has already painted the class, and reading matchMedia
during the first client render would diverge from the server's.
- Both secret tables passed `mutate` straight to onClick, so React handed it the
  MouseEvent. It is arity-0 and swallows the extra argument today, but the shape
  is a trap the moment the hook grows a parameter; call it explicitly.
- The format/provider tags painted `bg-[var(--ag-c-0517290F)]`. That literal is
  the compat shim for colorFillTertiary — identical in dark, within 2% alpha in
  light — so use the token, which /m also defines.
- The delete-dialog hand-off cast through `unknown` for no reason:
  `NamedSecretRow extends LlmProvider`, so the row already IS that shape.
- Four `error: any` mutation handlers now take `unknown` and read the payload
  through the shared `extractApiErrorMessage`, keeping the per-action fallback.
- The page-shell heading sized itself off bare `--ant-*` variables, which only
  exist where antd's ConfigProvider emits them. /m has no antd, so the whole
  declaration was invalid there and the h1 fell back to the UA size; each var
  now carries the literal the desktop's token config resolves to.

Imports reordered to satisfy the package's lint config, and Tag/LLMIconMap now
come from their subpaths rather than the antd-backed root barrel.
…e field

Both regressions arrived with the extraction: the reset that used to live in the
mutation's onSuccess could not follow the mutation into the package, and the
rename seed moved from openRenameModal into an open-callback.

Create: resetting in onCancel missed every other way the dialog closes — Escape,
the overlay, and a successful submit all left the last input sitting there for
the next open. `afterClose` covers all of them and still fires while the form is
mounted.

Rename: EnhancedModal accepts antd's `afterOpenChange` in its prop type but never
calls it, so the field was opening empty. Seed it with `initialValues` and a key
that remounts the form when the target project or its name changes.
…stamp

`"YYYY-MM-DD H:mm:sssAZ"` is not a shape any API emits — `sss` is not a dayjs
token, and even read charitably it demands milliseconds AND a trailing offset.
Of five realistic inputs only `...T14:32:00.000Z` parsed; `...T14:32:00Z`,
`+02:00`, `YYYY-MM-DD HH:mm:ss` and microsecond precision all returned an
Invalid Date. Fall back to the same tolerant chain formatDay walks, which
resolves every one of them to the right instant.

formatDay itself is fine — verified against the same inputs, each lands on the
correct instant via the fallback list.
Import order across the moved files, prettier over ThemeContextProvider's import
block, and the getAgentaApiUrl import in services/api.ts that went unused when
the API-keys service moved into @agenta/settings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant