feat(frontend): @agenta/settings + @agenta/settings-ui — the settings spine leaves the app - #5885
feat(frontend): @agenta/settings + @agenta/settings-ui — the settings spine leaves the app#5885ardaerzin wants to merge 18 commits into
Conversation
|
@coderabbitai review |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughChangesThe PR adds shared Shared platform
Estimated code review effort: 5 (Critical) | ~90 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
fe74b64 to
2a8c549
Compare
b21dda1 to
d786317
Compare
2a8c549 to
4218be4
Compare
d786317 to
6cf00f1
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (12)
web/packages/agenta-settings-ui/src/AccountPage.tsx (1)
126-131: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winEnforce the typed-email gate inside
onConfirm.The component documents that it owns the typed-email gate.
onConfirmcallsonDeleteAccount()without checkingconfirmed, 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 valueConsider a named export for consistency.
AccountPage,ApiKeysPage,PreferencesPage, andThemePickerall use named exports. This file uses a default export. A named export keeps the package surface uniform and makes re-exports insrc/index.tssymmetric.♻️ Proposed change
-const SettingsPageShell = ({ +export const SettingsPageShell = ({-export default SettingsPageShellAlso applies to: 107-107
web/packages/agenta-settings-ui/src/projects/ProjectsPage.tsx (1)
19-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport
ProjectFormValues.
ProjectDialogState<ProjectFormValues>appears in the exportedProjectsPagePropssignature, butProjectFormValuesstays module-private. A host that implementsrenderCreateDialogorrenderRenameDialogcannot 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 valueMove the
ProviderRowinterface 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 inNamedSecretTable.tsxat Lines 58-61.web/packages/agenta-settings-ui/src/webhooks/WebhooksPage.tsx (3)
16-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMerge 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-fixfrom thewebdirectory before committing."Sources: Coding guidelines, Pipeline failures
50-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSimplify the repository regex.
Inside a character class,
\/is a useless escape.eslint no-useless-escapereports this pattern. Use/repos\/([^/]+\/[^/]+)\//.
146-151: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueMemoize the per-row toggle handler.
handleToggle(record)returns a new function on every render, soActiveTogglere-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 winReuse the shared
ProjectFormValuestype instead of duplicating it.
web/packages/agenta-settings-ui/src/projects/ProjectsPage.tsxlines 9-12 declare the identical interface. Two copies can drift, and the OSS forms feedonSubmitof the shared page. Export the type from@agenta/settings-uiand import it here.web/oss/src/components/pages/settings/assets/navigation.ts (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNarrow this re-export to the navigation surface.
export * from "@agenta/settings"exposes the whole package through a navigation module. Hooks such asuseApiKeysanduseStaticTable, the API-key service, and the React access helpers all become reachable fromsettings/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/settingsand delete this shim.web/packages/agenta-entities/src/webhook/atoms.ts (1)
40-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider pruning
webhookDeliveriesAtomFamily.
atomFamilyretains one atom for everywebhookSubscriptionIdpassed to it, including deleted subscriptions. The family has noremovecall and no customshouldRemove, 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. AddwebhookDeliveriesAtomFamily.setShouldRemove(...)or callremove(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 winMemoize the
themeandflagsprops.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
PreferencesPagereceives 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 winReplace 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/webhookbarrel, soservices/webhooks/api.ts,services/webhooks/types.ts, andstate/webhooks/atoms.tsexpose 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.tsalready 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 withexport 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
⛔ Files ignored due to path filters (1)
web/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (72)
web/mobile/next.config.tsweb/mobile/package.jsonweb/mobile/src/features/settings/SettingsScreen.tsxweb/mobile/src/styles/globals.cssweb/oss/next.config.tsweb/oss/package.jsonweb/oss/src/components/Layout/ThemeContextProvider.tsxweb/oss/src/components/Webhooks/utils/handleTestResult.tsweb/oss/src/components/pages/settings/APIKeys/APIKeys.tsxweb/oss/src/components/pages/settings/APIKeys/assets/constants.tsweb/oss/src/components/pages/settings/Account/DeleteAccount.tsxweb/oss/src/components/pages/settings/Preferences/Preferences.tsxweb/oss/src/components/pages/settings/Preferences/components/ThemePicker.tsxweb/oss/src/components/pages/settings/Projects/index.tsxweb/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsxweb/oss/src/components/pages/settings/Vault/NamedSecretTable/index.tsxweb/oss/src/components/pages/settings/Webhooks/Webhooks.tsxweb/oss/src/components/pages/settings/assets/navigation.tsweb/oss/src/components/pages/settings/components/SettingsPageShell.tsxweb/oss/src/components/pages/settings/hooks/useSettingsAccess.tsweb/oss/src/components/pages/settings/hooks/useStaticTable.tsweb/oss/src/lib/helpers/dateTimeHelper/index.tsweb/oss/src/services/api.tsweb/oss/src/services/apiKeys/api/index.tsweb/oss/src/services/project/index.tsweb/oss/src/services/project/types.tsweb/oss/src/services/webhooks/api.tsweb/oss/src/services/webhooks/types.tsweb/oss/src/state/webhooks/atoms.tsweb/oss/src/state/webhooks/state.tsweb/oss/tailwind.config.tsweb/packages/agenta-entities/package.jsonweb/packages/agenta-entities/src/profile/index.tsweb/packages/agenta-entities/src/project/api.tsweb/packages/agenta-entities/src/project/index.tsweb/packages/agenta-entities/src/project/types.tsweb/packages/agenta-entities/src/webhook/api.tsweb/packages/agenta-entities/src/webhook/atoms.tsweb/packages/agenta-entities/src/webhook/handleTestResult.tsweb/packages/agenta-entities/src/webhook/index.tsweb/packages/agenta-entities/src/webhook/state.tsweb/packages/agenta-entities/src/webhook/types.tsweb/packages/agenta-settings-ui/eslint.config.mjsweb/packages/agenta-settings-ui/package.jsonweb/packages/agenta-settings-ui/src/AccountPage.tsxweb/packages/agenta-settings-ui/src/ApiKeysPage.tsxweb/packages/agenta-settings-ui/src/PreferencesPage.tsxweb/packages/agenta-settings-ui/src/SettingsPageShell.tsxweb/packages/agenta-settings-ui/src/ThemePicker.tsxweb/packages/agenta-settings-ui/src/index.tsweb/packages/agenta-settings-ui/src/projects/ProjectsPage.tsxweb/packages/agenta-settings-ui/src/secrets/NamedSecretTable.tsxweb/packages/agenta-settings-ui/src/secrets/SecretProviderTable.tsxweb/packages/agenta-settings-ui/src/webhooks/WebhooksPage.tsxweb/packages/agenta-settings-ui/tsconfig.jsonweb/packages/agenta-settings/eslint.config.mjsweb/packages/agenta-settings/package.jsonweb/packages/agenta-settings/src/access.tsxweb/packages/agenta-settings/src/api/apiKeys.tsweb/packages/agenta-settings/src/index.tsweb/packages/agenta-settings/src/navigation.tsweb/packages/agenta-settings/src/useApiKeys.tsweb/packages/agenta-settings/src/useStaticTable.tsweb/packages/agenta-settings/tests/unit/navigation.test.tsweb/packages/agenta-settings/tsconfig.jsonweb/packages/agenta-settings/vitest.config.tsweb/packages/agenta-shared/package.jsonweb/packages/agenta-shared/src/utils/dateTime/dayjs.tsweb/packages/agenta-shared/src/utils/dateTime/index.tsweb/packages/agenta-ui/package.jsonweb/packages/agenta-ui/src/theme/index.tsweb/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
| <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> |
There was a problem hiding this comment.
📐 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 -30Repository: 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.cssRepository: 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.cssRepository: 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/srcRepository: 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.
| // 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 |
There was a problem hiding this comment.
🎯 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.tsRepository: 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
| 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> | ||
| )} |
There was a problem hiding this comment.
🎯 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.
| 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> | |
| )} |
| 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) |
There was a problem hiding this comment.
🗄️ 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-sharedRepository: 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 300Repository: 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 260Repository: 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 180Repository: 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))}")
PYRepository: 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))}")
PYRepository: Agenta-AI/agenta
Length of output: 704
Migrate both API boundaries to Fern and runtime validation.
web/packages/agenta-entities/src/profile/index.ts: addgetUsersClienttoweb/packages/agenta-sdk/src/resources.ts, callfetchUserProfile(), and validate the response withsafeParseWithLogging.web/packages/agenta-entities/src/project/api.ts: usegetProjectsClient()for all five operations, passworkspace_idthrough{queryParams: {...}}, and validate every response withsafeParseWithLogging.
📍 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
| 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, | ||
| }) |
There was a problem hiding this comment.
📐 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/packagesRepository: 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 200Repository: 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
fiRepository: 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
doneRepository: 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 300Repository: 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
| 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" |
There was a problem hiding this comment.
📐 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
| 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]) |
There was a problem hiding this comment.
🔒 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' webRepository: 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 200Repository: 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 200Repository: 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")
JSRepository: 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.tsRepository: 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.tsRepository: 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
| 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 |
There was a problem hiding this comment.
🎯 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/ossRepository: 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 300Repository: 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.jsonRepository: 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:
- 1: https://day.js.org/docs/en/plugin/custom-parse-format
- 2: https://github.com/dayjs/dayjs-website/blob/master/docs/plugin/custom-parse-format.md
- 3: https://day.js.org/docs/en/parse/string-format
- 4: https://github.com/dayjs/website/blob/master/docs/parse/string-format.md
- 5: CustomParseFormat + strict parsing issue iamkun/dayjs#905
- 6: https://day.js.org/docs/en/parse/is-valid
🏁 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 || trueRepository: 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)}))
}
JSRepository: 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.
| 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" | ||
| } |
There was a problem hiding this comment.
📐 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' webRepository: 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 || trueRepository: 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
| // 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]) |
There was a problem hiding this comment.
🎯 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.*' webRepository: 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.tsxRepository: 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",
})
PYRepository: 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.
4218be4 to
e982ee7
Compare
6cf00f1 to
cbe2cc2
Compare
Settings was the last large surface still owned entirely by the app layer, and
/mhad nosettings 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 sharedpage/tab model.
@agenta/settings-ui— the rendered half: the settings shell, Preferences, Account, the secretand vault tables, the webhooks page, the Projects table.
@agenta/entitiesgains the profile and webhook entity layers;@agenta/entities/profileiswhat lets
/mshow a real Account tab.APIKeyssplits intouseApiKeys+ApiKeysPage, so the mobile app can reuse the hook withoutthe desktop page.
/mgets a real settings page — shared shell, shared Preferences — plus a settings rail, andthe settings packages are registered with Tailwind so their classes are not purged.
Twelve commits; the last is the lockfile for
settings-ui'sentity-uiand react-query deps.Not run in a browser — static gates only (
pnpm lint-fix24/24,tsc --noEmitcleanfor
@agenta/shared,ui,entities,entity-ui,settings-ui,oss,ee,mobile).Stacked on
pkg/agent-overview-body; review only this lane's diff.