Three QA-found edge cases in the provider form:
- Choosing "Use <x> as custom" set the model without running onOptionSelect, so
reasoning/price stayed stale for the typed model. Fire onOptionSelect with the
synthetic option so they reset like a dropdown pick.
- Selecting effort max/xhigh on an adaptive-capable model with mode unset left
the backend with no way to route the reasoning (it dropped it). Auto-set mode
to Adaptive, since max/xhigh are adaptive-thinking effort levels.
- Add zod cross-field refines (minLength<=maxLength, reasoning.maxTokens<=32000)
that native HTML5 validation can't express, mirroring the new backend checks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onOptionSelect reset reasoning.mode and reasoning.effort when the agent model
changes but left reasoning.maxTokens untouched, so a budget token value set for
a previous model leaked into the next one (e.g. switching from a budget-capable
model to an adaptive-only model kept the stale value in the form). Reset it
alongside mode/effort. Found during comprehensive provider QA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks external PR #328 (octo-patch). The PR added only the provider core;
this brings MiniMax to full parity with the other providers (qwen) so it is
selectable and configurable in the UI and installer.
Applied from the PR (verified against MiniMax's official API docs — M3/M2.7/
M2.7-highspeed are real current models; corrected the M3 description from the
PR's "512K" to the documented ~1M context):
- minimax provider package (OpenAI-compatible https://api.minimax.io/v1),
config.yml, models.yml, tests; MINIMAX_API_KEY/SERVER_URL/PROVIDER env vars;
ProviderMiniMax type + DefaultProviderNameMiniMax; providers.go wiring;
Valid() whitelist.
Added for completeness:
- goose migration adding 'minimax' to the PROVIDER_TYPE enum + database
ProviderTypeMinimax const.
- GraphQL: minimax in ProviderType enum, ProvidersModelsList,
ProvidersReadinessStatus, DefaultProvidersConfig; resolvers wire default
config/models + enabled status; gqlgen regenerated.
- Frontend: MiniMax icon (lobehub), provider-icon + settings-providers
registration + provider type list; regenerated GraphQL types.
- Installer wizard: provider form, screen, list, registry, env-var mappings,
locale strings + help text.
- ctester/ftester: -type/-provider minimax support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ReasoningFields mode/effort selects used `defaultValue`, making them
uncontrolled — Radix reads it once on mount, so the `setValue` resets in
onOptionSelect (reset reasoning on model change) updated form state but never
the visible selection. Result: after switching models the selectors showed
stale values (e.g. mode "Adaptive" when the field was cleared to null; effort
left blank when a stale 4.7/4.8 "xhigh" had no item under opus-4-6). Switch to
controlled `value` so the display follows the reset. Also drop a dead
empty-comment block in onOptionSelect.
Found during Layer 3 live verification on the docker stack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The component declared control as Control<FormData> (z.output, required
name/type) but receives the form's Control<FormInput> (z.input, optional
name/type), which `tsc -b` (the production build typecheck) rejects with
"Type 'undefined' is not assignable to type 'string'". `tsc --noEmit -p
tsconfig.json` did not catch it; `pnpm run build` (tsc -b && vite build) now
passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switching an agent's model left the prior reasoning.mode/effort in the form
(onOptionSelect only reset price), so a stale budget mode or an effort the new
model doesn't support could be persisted. Now the model picker also resets
reasoning: adaptive-only models lock to adaptive, others clear mode + effort.
Verified: tsc --noEmit + eslint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the per-agent reasoning UI to support adaptive thinking, gated by each
model's declared capability (the reasoning fragment) instead of a model-name
regex:
- new ReasoningFields component: a reasoning Mode select (adaptive | budget,
shown only for adaptive-capable models, locked to adaptive for adaptive-only)
and an effort select whose options follow the model's allowed efforts
(incl. xhigh/max).
- getReasoningEffort handles xhigh/max; getReasoningMode + the form schema and
transformFormToGraphQL carry the chosen mode through to the API.
Verified locally: tsc --noEmit and eslint clean. Runtime verification on the
stack (Layer 3) is the next step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds reasoning { mode efforts } to the modelConfigFragment and regenerates
GraphQL types, so the provider settings UI can gate the reasoning mode/effort
controls by each model's declared capability (ModelReasoningMode incl.
adaptive_only) instead of a model-name regex.
Groundwork for the adaptive-thinking settings UI port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the backend half of PR #288 onto our branch (the frontend is
reworked separately in our codegen style), 3-way merged so the #233
config/models-path changes are preserved:
- adaptive_thinking.go: smithy Build middleware that rewrites the langchaingo
Converse body from thinking{type:enabled,budget_tokens} to
thinking{type:adaptive} + output_config.effort, wired via WithAPIOptions and
prepareCallOptions in Call/CallEx/CallWithTools.
- pconfig: ReasoningConfig.Mode (adaptive|budget) + EffectiveMode/IsZero.
- GraphQL: ReasoningMode enum, reasoning.mode field, xhigh/max effort levels.
Drops the dead llms.WithMetadata adaptive branch from AgentConfig.BuildOptions
(nothing reads opts.Metadata on the Bedrock path; adaptive is applied per-call
by the provider) and updates the unit test accordingly.
Hardening still pending (next commit): strip temperature/top_p/top_k and set
display:summarized for adaptive requests, add Opus 4.7/4.8 catalog entries with
a model reasoning-capability descriptor + force-adaptive backstop, and a live
Bedrock repro. Until then Opus 4.7 should not be selected for an agent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commitlint had no config anywhere (no config file, no husky commit-msg
hook), so its rules were never loaded; the "commit" script also pointed
at an uninstalled binary. Remove @commitlint/cli and
@commitlint/config-conventional, the dead "commit"/"commitlint" scripts,
and the stale commitlint entry in README Development Requirements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed unused via depcheck + manual verification:
- anser, js-cookie, @types/js-cookie — no imports anywhere
- rehype-raw — only referenced in a vite manualChunks regex, never
imported as a markdown plugin (markdown.tsx uses rehype-highlight/slug
and remark-gfm); also dropped from that regex
- @graphql-codegen/{client-preset,near-operation-file-preset,typescript}
— codegen config uses explicit plugins (typescript-operations,
typed-document-node), not these presets/base plugin
Also drop the dead package.json "eslintConfig" block — it is ignored by
the flat config (eslint.config.mjs) and referenced an uninstalled
storybook plugin.
Verified: graphql:generate reproduces src/graphql/types.ts unchanged,
plus build, lint, 604 tests, and --frozen-lockfile all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The plugin was a direct devDependency but never enabled in
eslint.config.mjs (0 active jsx-a11y rules via --print-config). Dropping
it removes dead weight and one stale eslint-9 peer constraint. Lint still
passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add frontend/.nvmrc (24.17.0) as the single source of truth for Node;
GitHub CI reads it via node-version-file, Dockerfile uses node:24.17.0-slim
- bump packageManager to pnpm@11.8.0; drop "corepack prepare pnpm@latest"
so the pnpm version derives from packageManager everywhere
- migrate pnpm onlyBuiltDependencies -> allowBuilds in pnpm-workspace.yaml
(the package.json "pnpm" field is no longer read by pnpm 11)
- add a CI step that fails if the Dockerfile Node tag drifts from .nvmrc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The directory expand/collapse control now shows the folder icon by
default and crossfades to a chevron on hover (motion, reduced-motion
aware); the single element toggles expansion on click. Nesting indent
gains the icon->text gap (22px/level) so a child's icon lines up under
its parent's label, and the header expand-all control moves to the
shared Button. Folder and chevron share the text-blue-400 accent set on
the parent (header chevron picks it up on hover).
Also prunes ~46 restatement/justification comments and a dead
collectAllFilePaths export (plus its tests) to match the house style.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prettier / Lint / Test were `continue-on-error: true` (advisory — failures did not fail CI), the same gap that let type errors pile up. Now Prettier, Lint, Type check and Test all block the lint-and-test job (which runs on every branch push).
Prerequisite: `prettier --write` on 5 pre-existing non-conformant files (pages/login.tsx, lib/report/report-pdf.tsx, 3 *.test.tsx) so the now-blocking Prettier check passes — pure formatting, no logic change. Install stays advisory (setup step); backend checks unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vite build strips types and never type-checks, and the old build`s bare tsc (solution config, files:[]) checked nothing — so type errors reached main unnoticed (which is how 76 had accumulated). Now they fail fast everywhere.
- build: `tsc -b && vite build` — `pnpm build` (and the Docker image build, which runs `pnpm run build`) fails on any type error before bundling.
- add `typescript` script (`tsc -b`, checks both app + node project configs).
- ci.yml: blocking "Frontend - Type check" step in lint-and-test (runs on every branch push; docker-build only runs on main/tags).
Verified: a type error makes both `pnpm run typescript` and `pnpm build` exit non-zero (vite never runs); removing it restores a clean build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The provider form had 23 errors rooted in Zod v4: `z.preprocess` gives an `unknown` input type, which @hookform/resolvers v5 surfaces as the form field-values type, mismatching `useForm<output>` and cascading to every Control<FieldValues> site.
- Replace `z.preprocess` with explicit helper schemas (proper input/output types) and use `useForm<FormInput, unknown, FormData>`; make the reusable field components generic `<T extends FieldValues>` (Control<T> + FieldPath<T>).
- Fetch `thinking` on the model-config fragment (regenerated types.ts; backend ModelConfig.thinking exists).
- Validate the provider `type` into the ProviderType enum via `z.nativeEnum(...).parse` at the GraphQL boundary instead of an `as` cast (the form intentionally uses watch() to keep disabled fields in the payload, so its values stay input-typed).
tsc: 23 → 0; the frontend now has zero TypeScript errors. 606 tests pass; eslint + prettier clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hack-free fixes (no `as any`/`@ts-ignore`/`as unknown as`):
- tsconfig lib es2023 for Array.findLast; tighten DocumentTitle TitleResolver to ApolloTitleComponent (the broad ComponentType was masked by findLast returning any).
- NodeJS.Timeout → ReturnType<typeof setTimeout> in browser code; models import casing (./User → ./user).
- settings prompt/api-token forms: type against the real generated fragment types and the Zod input/output split (useForm<Input, ctx, Output>); generic FormTextareaItem<T>.
- data-table / detail-nav: noUncheckedIndexedAccess guards; React vs DOM KeyboardEvent disambiguation; drop invalid user-event delay option.
tsc: 76 → 23 (the remaining 23 are one settings-provider Zod/RHF cluster). 606 tests pass; eslint + prettier clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TypeScript 6.0 deprecated `baseUrl` (removed in 7.0). The `@/*` alias resolves relative to each tsconfig without it, and Vite resolves `@` via its own resolve.alias, so baseUrl was vestigial. Dropping it removes the need for `ignoreDeprecations: "6.0"`, which editor-bundled TypeScript (5.x) flagged as an invalid value (TS5103).
Also drop the dead `@env`/`./env.ts` entry from tsconfig.node.json: the file does not exist and the alias is imported nowhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the deprecated typescript-react-apollo codegen plugin (unmaintained for Apollo Client v4) with typed-document-node. All ~105 useXxxQuery/Mutation/Subscription call sites are rewritten to the generic useQuery/useMutation/useSubscription(XxxDocument, ...) form, with skipToken replacing skip + conditional-variables.
graphql-codegen.ts: plugins typescript-operations + typed-document-node; add scalars { Time: string } (was unknown); drop withHooks/apolloReact* options. tsconfig.app/node.json: ignoreDeprecations "6.0" for the baseUrl TS5101 deprecation. Regenerate src/graphql/types.ts.
Behavior-preserving: variables, fetchPolicy, error handling, and subscription onData/refetchQueries are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /knowledges list query fetches documents with `withContent: false` to save bandwidth, writing an empty `content` to the shared normalized KnowledgeDocument cache entity. That clobbered the full body loaded by the detail query, so opening a document by direct URL (or reload) showed an empty editor.
Add a `content` field merge policy so an empty incoming value never blanks out a body already loaded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename a knowledge document via a dedicated mutation that rewrites only the
question in cmetadata — no re-embedding, no embedder required — mirroring the
flows renameFlow pattern instead of round-tripping the full document through
updateKnowledgeDocument.
Backend:
- renameKnowledgeDocument(id, question) mutation + resolver (admin/user split;
ownership enforced at GetUserDocument, like the update pair)
- metadata-only query UpdateKnowledgeDocumentMetadata (no migration)
- unit + edge tests: metadata-only, missing-doc error, non-owner rejection
Frontend:
- renameKnowledge provider method; wire list and detail inline-rename to it
- drop the content "Preview" column and request the list with withContent:false
so it no longer pulls full document bodies
Verified end to end against a local Docker backend (rename works; content and
embedding preserved) and against the remote backend (graceful failure where the
mutation is not yet deployed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace 13 `any` usages with real shapes: ProviderTest / ProviderTestResults
for the test-mutation payload, Control<FieldValues> for the form control, and
inferred element types in the result maps. Collapse the two duplicated
3-level-nested error-formatting blocks into one recursive formatFormErrors
helper — validation error output is byte-identical (verified against the same
inputs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
react-hooks v6 ships this rule, but it only carries signal once the React
Compiler is enabled; until then it flags every RHF watch() and useReactTable
as unactionable noise (12 hits). Turn it off project-wide and drop the
now-redundant inline disable in use-element-virtual-list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CJK-detection regexes embedded a literal U+3000 ideographic space inside
their character class, which ESLint flagged as no-irregular-whitespace (error).
Switch the fullwidth ranges to \u escapes ( -〿-) — identical
match behaviour (verified: Han / U+3000 / fullwidth match, ASCII does not), no
irregular whitespace in source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No consumer reads the returned `virtualizer` (data-table destructures only the
spacer / measure fields), and exposing the raw instance leaks the abstraction —
the same dead, over-broad return just removed from useElementVirtualList. Drop it.
Also correct a stale doc claim: react-virtual does have an `enabled` option (it
gates the scroll listeners), the hook just doesn't surface it — so the
conditional-mount guidance stands without the false "no enabled option" note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sheet rendered every filtered option (1797 on a busy flows list), so the
filter re-render and the DOM weight (~1800 nodes) made filtering jank. Above a
100-item threshold the listbox now virtualizes via a new element-scroll
useElementVirtualList hook (the inner-container analogue of useWindowVirtualList):
only the ~30-node visible window is mounted. Small lists — and JSDOM tests —
keep rendering in full.
Roving focus onto an off-screen option scrolls it into view and focuses it once
its row mounts (pendingFocusId), so arrows/Home/End and open-time focus on the
current item work across the virtualized window. Options carry aria-setsize /
aria-posinset so screen readers still see the full count and position.
Measured live (chrome-devtools, test.pentagi.net, 1797-flow sheet): DOM nodes
~1800 → ~32; typing INP 258ms → ~200ms. The remaining cost is the flow.tsx page
re-render when the controller's filtered list changes (debounce flush) — a
separate concern. Open / End / Home / typing focus all verified; full suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typing in the sheet's search lagged badly (INP ~600ms on a 1797-item list): the
input bound to the controller's searchQuery, whose state lives in the page-level
hook (flow.tsx), so every keystroke re-rendered the whole detail page AND rebuilt
all ~1800 option rows before the caret could echo — characters appeared in batches.
Mirror the input value in local sheet state (instant caret echo, re-renders only
the sheet) and push to the controller in a transition (filtering + prev/next stay
correct; the heavy re-render no longer blocks the keystroke). Memoize the option
rows so a keystroke that only changes the local mirror does not rebuild them.
Measured live (chrome-devtools, test.pentagi.net, 1797-flow sheet): typing INP
606ms -> 258ms; all characters land, focus stays on the input. The residual is the
DOM reconciliation when the filter changes — addressed next by virtualizing the list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Focus was driven off `focusedId` through an effect, so a reconciliation-driven
change (the filter shrinking and re-pinning to the first survivor) also moved
DOM focus, yanking it off the search input mid-type. Two document.activeElement
guards (the second added in 5a7bdca) papered over the timing; the rAF re-check
only ever fired under JSDOM and was dead in real browsers — verified live.
Move focus imperatively only where the user navigates: opening the sheet now
uses Radix's onOpenAutoFocus (preventDefault + focus the current option, with no
rAF race against the focus trap), arrow keys focus synchronously in the key
handler, and render-phase reconciliation only updates the roving tabindex. Both
guards, the effect and the rAF are gone, and the previously-flaky focus-steal
test is deterministic (10/10 isolated).
Verified live (chrome-devtools, test.pentagi.net, 1797-flow sheet): open focuses
the current option, typing keeps focus on the input with no dropped chars, and
arrows / ArrowDown-from-input move focus.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The account page tracked a single `editing` section, so opening one editor
unmounted any other open form and silently discarded its unsaved input. Track
an open-section set instead: opening a section no longer closes the others, so
a half-typed draft survives switching between name, email and password.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The forced password-change screen reused the compact account-card footer, so it
rendered small right-aligned buttons that clashed with the full-width "Sign in"
CTA on the same login page. Add two orthogonal props: layout
('horizontal' default | 'vertical') drives footer direction and button width;
buttonSize ('default' default, mirroring Button) drives size. The forced screen
passes layout="vertical" (full-width stacked buttons, submit on top, matching the
login button language); the account cards pass buttonSize="sm" to stay compact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isModal (gated the Cancel button) and showSkip (gated the Skip button) were
redundant with the onCancel/onSkip callbacks they always paired with: every
call site that set a flag also passed the matching handler, and none passed a
handler while wanting the button hidden. Render each button from its callback's
presence instead. Removes two props and a name that misdescribed its job
(isModal gated a button, not a modal).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first migration pass missed call sites where the path is nested inside a
helper rather than passed straight to navigate/to: detail-navigation getHref
helpers, mergeHrefWithSearchParams row-open handlers, the knowledge create
redirect, the flow report window.open targets (wiring up the unused
routes.flowReport builder), and the OAuth return_uri. All now build from routes.
Add routes.test.ts: every builder output is asserted to match the app.tsx route
pattern that serves it via matchPath, guarding URL<->pattern sync, plus exact
checks for query encoding and the login return-url guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 3 of the route-registry migration: every /login redirect and post-login landing
fallback builds from lib/routes instead of hardcoded strings.
- protected-route, axios 401/403 interceptor, user-provider (logout, session-expiry,
/login + public-route checks) -> routes.login(...) / routes.login()
- oauth-result error redirect -> routes.login()
- login-form / public-route / login page post-login fallback -> routes.newFlow
getReturnUrlParam now has a single in-app caller (the routes.login builder) plus the
logout suffix-override; dropped its now-unused imports from axios and protected-route.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 2 of the route-registry migration: the provider/prompt settings pages build their
navigate() targets from lib/routes instead of hardcoded strings.
- settings-providers: provider(id), newProvider({ id | type }), newProvider()
- settings-provider: routes.settings.providers (x5)
- settings-prompts: routes.settings.prompt(name)
- settings-prompt: routes.settings.prompts (x2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce lib/routes.ts as the single source of truth for client route paths, with typed
builders for parameterised routes (flow, provider, prompt, login return-url, ...). First
migration step covers the settings shell we own:
- app.tsx settings index/catch-all redirects -> routes.settings.account
- main-sidebar Settings/Profile links -> routes.settings.root / .account
- settings-layout menu items + the "Back to App" fallback -> routes.*
The rest (settings provider/prompt pages, auth redirects, main nav) migrate in follow-ups.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Account was added as the first settings sidebar item, but the index and catch-all
routes still redirected to /settings/providers, so the gear "Settings" link dropped
the user on the second item. Point both redirects at /settings/account.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(displayName).charAt(0) returns the first UTF-16 unit, so a non-BMP first character
(emoji, rare CJK) rendered as a lone surrogate (�). Spread to iterate by code point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The settings shell's only exit was a "Back to App" link hardcoded to /flows, so opening
Settings or Profile from anywhere dropped the user at the flows list on the way out.
- the Settings and Profile entry links pass the current path as location.state.from
- SettingsLayout captures it once on entry (surviving sub-tab navigation, which drops
state) and points "Back to App" there, falling back to /flows
- tests: SettingsLayout honors the origin / falls back / preserves it across sub-tabs;
MainSidebar's Settings and Profile links carry the origin
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three account forms duplicated the getApiErrorCode + ERROR_BY_CODE lookup pattern
verbatim, and each carried a dead `AuthRequired` entry that the axios interceptor
already handles (hard redirect) before the form's catch can render it.
- add resolveApiErrorMessage(err, map, fallback) next to the axios error helpers
- email/name/password forms use it and drop the unreachable AuthRequired entry
- unit-test the helper's precedence (mapped code > server msg > fallback)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A network blip or 5xx on an /info refresh — after an email/name change or on plain
navigation — wrongly cleared local auth and bounced the user to /login, even though the
session cookie was still valid (the axios interceptor already handles real 401/403).
- refreshAuthInfo and the navigation refresh now keep the current session on a transient
/info failure instead of clearing auth and redirecting to /login
- add patchUser so the email/name forms reflect the change immediately, dropping the
dependency on a successful /info round-trip for the visible field
- tests: UserProvider keeps the session on transient failure (refresh and navigation
paths); forms call patchUser with the new value
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- settings-account: local accounts expose name/email/password; OAuth accounts hide
the password card and email editing, keep the name editable, and label the provider
(known label, raw fallback, then generic); renders nothing without a user
- name-change-form (previously untested): seeds the current name, submits the trimmed
value and refreshes auth, blocks an empty name, maps Users.NotFound to friendly copy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vmail validator rejected addresses the frontend's z.string().email() accepts
(uppercase, TLDs longer than 4 chars like .cloud), so users saw a confusing 400.
- relax the vmail regex to allow uppercase and TLDs of 2+ chars
- lowercase + trim the new address in ChangeEmailCurrentUser and in the form schema
so storage, the duplicate check, and login lookups stay case-stable
- validator + change-email tests for mixed case and long TLDs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- authLoginCallback matches users by email alone so an OAuth login links into an
existing (incl. local) account instead of 500-ing on users_mail_unique
- relink on a create-branch unique-violation race instead of returning 500
- issue the session with the linked account's actual role privileges
- clear the stale OAuth provider link when a user changes their email
- map the email-change unique-violation race to 409 instead of 500
- isUniqueViolation matches Postgres and SQLite case-insensitively
- tests for link/create/blocked/role-inheritance/race, email 409, provider reset
- update auth form test selectors after the form refactor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The account page is already available to OAuth users, so drop the
local-only gate on the user-menu "Profile" item to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Keep /settings/account available to OAuth users: drop the redirect and
the local-only sidebar gate. Show the auth provider (Google/GitHub) on
the summary badge, render the email read-only, and hide the password
card for OAuth accounts. Fix the `provide` -> `provider` typo in User.
- Add a "Display name" card + NameChangeForm (PUT /user/name) for all users.
- Make the email/password forms consistent: lead with Current Password,
unify placeholders ("Enter your ..."), drop the redundant Current Email
field, and size form buttons to match the "Change" buttons.
- Rename the user-menu item "My Profile" -> "Profile".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monaco-terminal.tsx had no importers; drop it together with the now-unused
monaco-editor and @monaco-editor/react dependencies (~3 MB). The component is
preserved on branch feature/frontend-monaco-terminal for later work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stabilizes a flaky test ("does not yank focus onto a listbox option when
filteredItems shrinks"): the rAF that moves roving focus now re-checks
document.activeElement before calling focus(), so a focus bounce between
the effect's gate check and the next frame no longer steals focus.
Provisional: the race only reproduces under JSDOM focus simulation (not in
real browsers), so the proper fix likely belongs in the test layer — to be
revisited separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>